け日記

最近はPythonでいろいろやってます

時系列データで使うPandas小技集

時系列データを扱うにあたって役に立った、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'])