Pandasでの日付・時間周りのちょっとしたチートシート

Apacheのログやアプリケーションサーバのログ分析をする機会がありまして、その際に出てきた定番の処理を自分用に再編成してまとめていきます。

# まずは日付・時間列の分離と結合

apacheログのサンプルです（生成ツールがあるんですね。。。）
>108.81.70.158 - - [16/Jun/2015:13:59:34 +0000] "GET /item/electronics/3717 HTTP/1.1" 200 86 "-" "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11"

>176.33.96.225 - - [16/Jun/2015:13:59:35 +0000] "GET /category/cameras HTTP/1.1" 200 88 "-" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11"

>32.129.29.109 - - [16/Jun/2015:13:59:35 +0000] "GET /item/toys/2278 HTTP/1.1" 200 129 "/search/?c=Toys" "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:10.0.1) Gecko/20100101 Firefox/10.0.1"

で、ここからapacheログをparseするのですが、区切り文字を正規表現で表して項目を分離してread_csvをやります。
詳細は先人たちのお知恵を借ります。

http://blog.mmast.net/read-apache-access-log-pandas


前提はここまで。
以下は純粋にpandasの話になります。
読み込んだapacheログはdfに読み込まれているものとします（pandas.DataFrame)。

# 日付と時間列　分離・結合
parserの書き方次第ですが「日付 時間」列になっていたり「日付」「時間」と分離されたりします。ものによっては最初から希望通りの列を得られない場合がありますので分離や結合をやってみます。

## 「日付　時間」列を「日付」列と「時間」列に分離する

~~~python
df[['date', 'time']] = df.loc['date_time'].apply(lambda x: pd.Series(str(x).split()))

~~~

## 「日付」「時間」列を「日付 時間」列に結合する（ついでにDatetimeIndex型にする）
~~~python
df[['date_time']] = df.DatetimeIndex(df[['date', 'time']].apply(lambda x: '{} {}'.format(x[0], x[1]), axis=1)
~~~
## 項目をインデックス化
DatetimeIndexを使わずにインデックス指定もできます。

~~~python
df.set_index['date_time']

~~~


続けてデータ加工処理をやっていきます。

# キーワード処理

## キーワード置換
特定の加工をするために正規表現などを用いてレコードの加工を行います。この部分はpythonに渡す前にsed などを用いて加工しても良いと思います（むしろそっちで）。
replaceメソッドを正規表現を使って実施します。メソッドは続けて実行が可能です。

~~~python
df_sel = df.replace("POST\s", "", regex=True).replace("GET\s", "", regex=True) 

~~~

## キーワード抽出
特定のキーワードを含むデータのみ選択します。同様に正規表現を使用できます。

~~~python
df_sel = df[df['request'].str.contains('AAAAA')]

~~~

#そもそもの入力レコードの件数チェック
対象データの件数が０件だったりすると後々面倒なことになります（型エラーだったりいろいろと）。
なので最小に対象レコード０件判定を行いバイパスするほうがシンプルな制御になります（たぶん）。

~~~python
import os
BASEDIR="/home/user/xxxxxxxxxxx"
FILE=os.path.join(BASEDIR, "apache.log")
len_FILE = sum(1 for line in open(FILE))

# 空 判定
if len_FILE > 0:
  xxxx
else:
  pass

~~~

# 他ファイルとの連携
Apacheログだけでなく内部処理を行うWebApplicationServerとのログ結合もしたいです。結合Key項目がありますので（ID）でMergeします。

## 他ファイルとの結合
~~~python
pd.merge(Apache, Wacs, on='ID')

#さらにこれをCSVファイルに出力します、インデックスはいらない
pd.merge(Apache, Wacs, on='ID').to_csv('BASEDIR + 'merged.csv', index=None)

~~~
# インデックス・カラム操作
## インデックスを再度割り振り
抽出したりマージするといろいろとインデックスが乱れます。
作り直したDataFrameのインデックスを割り当て直します。

~~~python
df.reset_index(drop=True, inplace=True)

~~~

## カラム名を変更する
~~~python
df.columns['datetime','id','requestId','requestime','userId']

~~~

# データ分析
いよいよデータ分析（といってもちょっとした集計）になります。

## 日付単位でリサンプル、ユニークユーザ数を日毎にカウントする
date_timeにインデックス設定状態でgroupbyを行っているので、ユーザ毎のカウント（列で）を行うためにinstack()してindexからcolumnにuserIDを戻してやります。

~~~python
df.groupby('userID').resample('D').mean().unstack().count()

~~~

# 3秒以上、reqtimeかかっているトランザクションの日別発生比率
対象と全量、それぞれ必要になりますね。
日付（インデックス）と比率がきれいに並びます。

~~~python
df_sel = df[df['reqtime'] > 3000].resample('D').['req'].count()
df_all = df.resample('D').['req'].count()
pd.concat([df_sel / df_all], axis=1)
~~~

# 指定時間あたりのトランザクション数を算出する

~~~python
# 対象時間を選定
START_TIME='2017-12-24 09:00:00'
END_TIME='2017-12-24 11:00:00'

# 対象時間データを抽出（２時間）
df_sel = df[(df['date_time'] >= START_TIME) & \
           [(df['date_time'] < END_TIME)] 

# 該当時間での日別件数と日別TPSを算出
pd.concat([df_sel.resamle('D').['req'].count(), df_sel.resamle('D').['req'].count() / (3600 * 2)], axis=1)
~~~

# 抽出条件と反転条件
ある条件に当てはまるユーザIDによるトランザクションを抽出する。処理時間が長い順に。

~~~python
# 当てはまる人たち
df_sel = df[df['userID'].str.contains('^A')].sort_values('reqtime', ascending=False)

# 当てはまらない人たち
df_sel = df[~df['userID'].str.contains('^A')].sort_values('reqtime', ascending=False)

~~~

# インデックスのちょっと繊細な操作
ここから先はデータにインデックスは設定してありません（日付、時間もデータの一部になります）。

## データ抽出するとインデックス番号が飛びます。

~~~python
# 対象時間データを抽出（２時間）
df_sel = df[(df['date_time'] >= START_TIME) & \
           [(df['date_time'] < END_TIME)] 

# 抽出後DataFrameのインデックス番号を振り直します
df_sel.reset_index()
~~~

同じインデックス番号でマージしたいときがあります。
何も指定せずにmergeすると（インデックスでなく別の何かのデータ列でマージ）、見事に期待するマージになりません。。。

~~~python
# 抽出処理などしたであろうDataFrameのインデックスを振り直します
df_1.reset_index()
df_2.reset_index()

# インデックス番号でマージ（お互いのDataFrameともに）
df.merge(df_1, df_2, left_index=Trune, ringht_index=True)

~~~

それってどんなとき？

例えばトランザックションの開始・終了を把握したいとしたとき、４つのトランザクションを１つの塊として把握する必要があるとします。
この場合１つ目のトランザクション開始時間と４つ目のトランザクション終了時間を抽出することになります。そして４つの塊がその後延々とログに出現する、、、状況で、かつrequestID毎にシリアライズされていると仮定します（かなりご都合の仮定ですが）。

## やること
０番目と３番目のサイクルでDataFrameから明細を取り出す。
つまり４で割って余りで分類することで一定パターンの処理を行う。

抽出条件
index % 4 = 0 →開始
index % 4 = 3 →終了

抽出したのでindexのリセット
index番号でmergeして開始時間・終了時間を横に並べる

以上の処理はこんな感じに。

~~~python
df_start = df[df.index % 4 == 0]
df_start.reset_index(drop=True, inplace=True]

df_end = df[df.index % 4 == 3]
df_end.reset_index(drop=True, inplace=True]

pd.merge(df_start, df_end, left_index=True, right_index=True)

~~~


レポートは以上です。自分用のチートシートとして書きました。
もうちょっとこう書くといいよってあればぜひご指摘をお願いします。