時系列データを扱うにあたって役に立った、Pandasのテクニックを紹介します。
- 文字列型のSeriesから日時型・日付型のSeriesへ変換する
- 日付に欠測値を含むデータを日毎に集計する
- 累積和を計算する
今回の例に使う時系列データは以下です。
ある商品の4/1〜4/3の購入履歴をイメージしてください。ユーザ(user_id)の購入日時(timestamp)と購入数(item_count)が入ったDataFrameとなっています。
import pandas as pd df = pd.DataFrame(...) print(df.info()) # <class 'pandas.core.frame.DataFrame'> # RangeIndex: 5 entries, 0 to 4 # Data columns (total 3 columns): # timestamp 5 non-null object # user_id 5 non-null object # item_count 5 non-null int64 # dtypes: int64(1), object(2) # memory usage: 200.0+ bytes
日時型や日付型への変換
timestampカラムは文字列になっていますので、最初に扱いやすい日時型へ変換する必要があります。
Seriesの型変換にはastype()がしばしば使われますが、文字列から日時へ変換することはできません。
代わりに、pandas.to_datetime()を使うと、Series型の各要素を日時型へ一括変換できます。
変換後は、Pythonのdatetime型ではなく、Pandasの提供するTimestamp型となります。このTimestamp型は、いろいろなフォーマットの日時文字列に対応しているため、上のような'/'区切りの日付でも問題ありません。
df['timestamp'] = pd.to_datetime(df['timestamp']) print(df.info()) # <class 'pandas.core.frame.DataFrame'> # RangeIndex: 5 entries, 0 to 4 # Data columns (total 3 columns): # timestamp 5 non-null datetime64[ns] # user_id 5 non-null object # item_count 5 non-null int64 # dtypes: datetime64[ns](1), int64(1), object(1) # memory usage: 200.0+ bytes
timestampは日時ですが、日単位で集計していきますので、日付型のカラム"date"を追加します。
Series型にはdtというアクセサが提供されており、Timestamp型を含む日時型の要素から、日付や時刻のみの要素へ一括変換できます。
df['date'] = df['timestamp'].dt.date print(type(df.loc[0, 'date'])) # datetime.date
日付に欠測があるデータを日毎に集計する
4/1〜4/3の3日間の、各ユーザの購入数を集計したいとします。
user_idとdateでgroupbyすれば良さそうですが、それでは購入していない日の購入数(つまり0)が取得できません。
# 以下では購入していない日('0001'は4/2、'0002'は4/3)の値が取得できない pd.DataFrame(df.groupby(['user_id', 'date']).sum()['item_count']).reset_index()
こうした場合、日付とユーザIDのみからなるDataFrameを最初に作成し、上の集計済みDataFrameと左外部結合して、欠測値を0埋める、という3段階に分けて行います。
最初に日付+ユーザIDのDataFrameの作成です。
pandas.date_range()を使うと、指定日時('2018-04-01')から一定の間隔(ここではfreq='D'のため、1日ごと)の日時型のSeriesを作ってくれます。periodsは繰り返し回数を指定し、ここでは3回となっています。
date_df = pd.DataFrame(pd.date_range('2018-04-01', periods=3, freq='D').date, columns=['date'])
ユーザIDのDataFrameも作ります。unique()で重複を除外したユーザIDの一覧を取得します。
user_id_df = pd.DataFrame(df['user_id'].unique(), columns=['user_id'])
この2つのDataFrameをクロスジョインで結合します。
merge()でサポートされるのはキーによる結合だけですので、ここでは2つのテーブルの全ての行が同じ値0となるカラムkeyを追加し、それをキーとして結合しています。これにより、一方のDataFrameの各行を、もう一方のDataFrameの全ての行と結合できます。結合後、カラムkeyは不要ですので、drop()で削除しておきます。
date_df['key'] = 0 user_id_df['key'] = 0 tmp_df = date_df.merge(user_id_df, on='key').drop('key', axis=1)
次に、上で作成したDataFrameと集計値のDataFrameをmerge()で左外部結合します。
購入していない日はNaNの値が入っていることがわかります。
sum_df = pd.DataFrame(df.groupby(['user_id', 'date']).sum()['item_count']).reset_index() sum_df = tmp_df.merge(sum_df, on=['date', 'user_id'], how='left')
最後に、fillna()で0埋めします。
sum_df = sum_df.fillna(0)
累積和を計算する
購入数の累積和を計算する場合、cumsum()が使えます。
以下では、日毎の購入個数の累積和を計算しています。
DataFrameは累積和を計算したい順にソートする必要があります。ここでは、インデックス(日付)順にソートすることで、4/2は4/1までの購入数+4/2の購入数、4/3は4/2までの購入数+4/3の購入数となるようにしています。
daily_sum_df = sum_df.groupby('date', group_keys=False).sum()['item_count'] daily_sum_df.sort_index() daily_sum_df.cumsum() # date # 2018-04-01 3.0 # 2018-04-02 6.0 # 2018-04-03 9.0 # Name: item_count, dtype: float64
さらに、groupbyと組み合わせると、ユーザごとの累積和も計算できます。
sum_df['user_cumsum'] = sum_df.groupby('user_id').cumsum()['item_count'] sum_df = sum_df.sort_values(['date', 'user_id'])