最近のビッグデータのうちの多くのものが、アクセスログ、イベントログなどのログデータです。その特徴として以下の2つがあります。
(1)多数の個体についての時系列データである(タイムスタンプを持つ)
(2)収集しているデータの大部分が(定型/非定型の)テキスト情報である
そこで今回はこのようなログデータをPythonで加工・分析するためのTip(小技・コツなど)をまとめました。
データの事例として、Kaggleにアップロードされたイベントログのデータ(https://www.kaggle.com/mehulkatara/windows-event-log) を用いました。
ここでは、「基本的なデータクリーニングや構文解析は終わっており、個体識別IDとタイムスタンプのマルチインデックスを持つPandasデータフレームとなっている」状態から始めます。
まずデータをデータフレームに読み込み、特定の個体のケースに絞り込みます。ここではdf.index.isin(キーワードのリスト)でインデックスに基づく検索を行っています。
import pandas as pd df12 = pd.read_parquet('eventlog.parquet') # 特定の個体のケースに限定する ch_machine = 'LAPTOP-1MKMTVPM' df2 = df12[df12.index.isin([ch_machine], level=0)]
定型テキストの出現頻度の推移をグラフ化する
次に、タイムスタンプから1時間毎といった「丸め時刻」を抽出し、ある定型テキストの出現頻度を丸め時刻毎に集計してグラフ化します。丸め関数は四捨五入のround()でなく切り捨てのfloor()を用います。演算によりデータフレームに新しい列を作る操作は、df['列名']への代入(警告が出ることがある)でなく、df.assign()メソッドを用いています。定型テキストの種別毎の合計度数が縦方向に並びますが、df.pivot()でそれらを横方向に展開しています。グラフ化は、pandasに用意されているdf['列名'].plot()メソッドを用いています。
# 丸め時刻毎に定型テキストの出現頻度を集計してグラフ化する # 日単位の丸め時間変数を作成する df21=df2.assign(time_round=df2.index.to_series().str[1].dt.floor('D')) # 日とEntryType毎に行数をカウントする df3 = df21.groupby(['time_round','EntryType']).count() df31 = df3.reset_index(); df32 = df31.set_index('time_round') # 各EntryTypeを列とする変数を作成する df4 = df32.pivot(columns='EntryType',values='ch_machine_name') df41 = df4.fillna(0) # 欠損値にゼロを代入する import matplotlib.pyplot as plt # エラーの発生の推移をグラフ化する from matplotlib import rcParams; rcParams['font.family']='MS Gothic' df41['Warning'].plot(figsize=(9,6)); df41['Error'].plot(figsize=(9,6)) plt.title('日別の警告とエラーの数 マシン: '+ch_machine) plt.legend(); plt.show()
以下のようなグラフが表示されます。
時間範囲を指定して検索する
次は、タイムスタンプ情報をもとに、ある期間に入っているデータの検索を行う事例です。ここではタイムスタンプをインデックスから一般の列に書き出した上で、別途設定した日付値との比較演算を行っています。尚、タイムスタンプにタイムゾーン情報がある場合は、ここでのquery()の比較演算はエラーになりますが、その場合はdf['列名'].replace(tzinfo=None)によりタイムゾーンの情報を消しておきます。
# 時刻(範囲)を指定したデータの検索 import datetime from datetime import timedelta # 日付、時、時間範囲を指定する query_date=20201023; query_time_h=8; query_hours=21 # 日付関連値を求める c_year=query_date//10000 c_month=(query_date//100)%100 c_day=query_date%100 # データ開始時刻の日付値 dt1=datetime.datetime(c_year,c_month,c_day,query_time_h,0,0) # データ終了時刻の日付値 dt2 = dt1 + timedelta(hours=query_hours) # インデックスから時間変数を取り出す df51 = df2.assign(c_time = df2.index.to_series().str[1]) # 指定した時間範囲内のデータに絞り込む df52 = df51.query('c_time >= @dt1 & c_time <= @dt2') df53 = df52.loc[:,['EntryType','Message','Source']]
この結果、以下のように行が絞り込まれたデータフレームが得られます。
定型テキストの出現傾向を表に要約する
数値データの場合はある時間間隔毎の集計値を見て傾向をつかみますが、テキストデータの場合は別の要約方法が必要となります。ここでは、ある時間間隔毎に、最も頻繁に出現した文字列をその時間間隔における「代表文字列」として、それを表の形式に集約し、テキストの出現傾向を把握しやすいようにします。ここでは(系列名).value_counts()にて文字列の出現頻度表を取得し、さらに.index[0]で最頻度の文字列を抽出しています。またここでもdf.pivot()を用いてデータを横方向に展開し、さらにmatplotlibのtable関数を用いて表を作成しています。メッセージなどのテキスト情報は一般的に長いので、表に表示するにあたって、文字数の切り詰めと改行の強制挿入を実施しています。
# 丸め時間区間毎に最も多く登場した定型テキストを表形式で表示する import textwrap # strftime()で日本語を使用するのに必要 import locale; locale.setlocale(locale.LC_ALL, '') def func_part_of_month(tround): '''月の上旬・中旬・下旬の判定関数''' if tround.day <=10 : return '上旬' elif tround.day <= 20 : return '中旬' else : return '下旬' df61 = df2.reset_index() # 年月を表す変数を導入 df62=df61.assign(yr_month=df61['timestamp'].dt.strftime('%Y年%m月')) # 上/中/下旬を表す変数を導入 df62=df62.assign(part_of_month=df62['timestamp'].apply(func_part_of_month)) # グループ毎に最頻出の文字列を求める df63=df62.groupby(['yr_month','part_of_month'])['Message'].apply(lambda x:x.value_counts().index[0]) df64 = pd.DataFrame(df63) def func_wrap(s): '''長い文字を切り詰め、途中に改行を入れる関数''' s_trunc = textwrap.shorten(s,100) s_wrap_list = textwrap.wrap(s_trunc, 25) return '\n'.join(s_wrap_list) # 文字列切り詰めと改行挿入 df64['f_msg'] = df64['Message'].apply(func_wrap) df65=df64.reset_index(); df66 = df65.set_index('yr_month') # 各part_of_monthを列とする変数を作成する df67 = df66.pivot(columns='part_of_month',values='f_msg') # 欠損値の表示方法の指定 df68 = df67.fillna('記録なし') # データの行数に応じて表の高さを決める tbl_height = len(df68) * 1.0 # 図形の描画 fig=plt.figure(figsize=(8,tbl_height)) ax=fig.add_subplot(111); ax.axis('off') # matplotlibのtable関数で表を描く tbl=ax.table(cellText=df68.values,bbox=[0,0,1,1],colLabels=df68.columns,rowLabels=df68.index) plt.plot()
以下のような表が出力されます。