はじめに
筆者は今AI Questという経産省主催のAI人材育成プログラムに参加しておりまして、そこで第1タームのコンペがちょうど本日の8:00に終わりました。結果は18位(参加295人)と飛びぬけてよかったわけではないですが、実データを想定した需要予測ということで非常に実践的な内容だったかと思うので、今回の経験を文章に落としておこうと思って書いておきます。
データの概要
今回のコンペの概要としてはある企業の2年間ほどの各商品各店舗に対する日ごとの売り上げデータが与えられて、それをもとに次の月の売り上げを予想するというもので、データは以下のような形で与えられていました。
日付 店舗ID 商品ID 商品価格 売上個数
0 2018-01-01 9 1000001 420 1.0
1 2018-01-01 6 1000001 420 1.0
2 2018-01-01 10 1000001 420 1.0
3 2018-01-01 0 1000017 250 1.0
前処理
pivotで扱う前に日付をdatetime型にしておきます。
df = pd.read_csv("data/sales_history.csv", parse_dates=["日付"])
このようにして読み込むと日付の列をdatetimeで読み込むことができました。df.info()
として表示してみると以下のようにdatetime型になっていることがわかります。
RangeIndex: 1119570 entries, 0 to 1119569
Data columns (total 5 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 日付 1119570 non-null datetime64[ns]
1 店舗ID 1119570 non-null int64
2 商品ID 1119570 non-null int64
3 商品価格 1119570 non-null int64
4 売上個数 1119570 non-null float64
dtypes: datetime64[ns](1), float64(1), int64(3)
memory usage: 42.7 MB
次にこれを月ごとに集計したいので年と月を合わせた列を作成します。
df["年月"] = df["日付"].dt.strftime("%y%m")
df["年月"].head()
としてみてみると次のように作成されていることがわかります。ちなみに%Y
にすると西暦4桁になり、%y
で下2桁になります。
0 1801
1 1801
2 1801
3 1801
4 1801
Name: 年月, dtype: object
それでは月ごとにまとめていきましょう。売上個数は合計でいいですが、価格は合計ではなく平均など(今回は異常値の影響を受けない中央値にした)を使いたいのでagg
でまとめて記述します。
df = df.groupby(["年月", "商品ID", "店舗ID"]).agg({"売上個数":"sum", "商品価格":"median"}).reset_index()
以下のように月ごとにまとめられました。ここで、.reset_index()
とするのでは、そのままではグループ化に使用した列がMultiIndex(インデックスが多段階の階層をもつような形)になってしまって扱いづらいのでそれを解消するようにしています。
年月 商品ID 店舗ID 売上個数 商品価格
0 1801 1000001 0 6.0 420
1 1801 1000001 1 2.0 325
2 1801 1000001 2 1.0 420
pivotテーブル
いよいよ本題です。今回やるべきこととしては、今は売り上げがない月は0ではなくデータそのものがない状態でありこのまま予測をしてしまうと売り上げが0だったという情報が抜け落ちてしまうので、データがない月を0で埋めたいです。また、ラグ特徴量(先月とかその前の月の売り上げを)を付け加えたいです。
これらの操作を簡単に行うことができるのがpivotテーブルです。
df_pivot = df.pivot_table(index=["商品ID", "店舗ID"], columns="年月", values="売上個数")
このようにインデックスやカラムと集計する値を指定してあげるといい感じに作ってくれます。
年月 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 \
商品ID 店舗ID
1000001 0 6.0 3.0 1.0 2.0 NaN 4.0 3.0 2.0 1.0 NaN
1 2.0 1.0 NaN 2.0 NaN 2.0 1.0 NaN NaN 1.0
2 1.0 NaN 1.0 NaN 1.0 2.0 3.0 4.0 2.0 1.0
こうすると見ての通り、データがない時を欠損値として表現できるので、これを0埋めすれば売り上げ0を表現することができました。
df_pivot = df_pivot.fillna(0)
次にラグ特徴量を見ていきます。書き方は時系列データで一列ずらすのと同じshiftです。
sold_lag_1 = df_pivot.shift(1, axis=1)
こうすると以下のように右に一個ずれてくれます。
年月 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 \
商品ID 店舗ID
1000001 0 NaN 6.0 3.0 1.0 2.0 0.0 4.0 3.0 2.0 1.0
1 NaN 2.0 1.0 0.0 2.0 0.0 2.0 1.0 0.0 0.0
2 NaN 1.0 0.0 1.0 0.0 1.0 2.0 3.0 4.0 2.0
同じようにして2,3カ月前のラグも特徴量として加えることができました。
sold_lag_2 = df_pivot.shift(2, axis=1)
sold_lag_3 = df_pivot.shift(3, axis=1)
最後にこれらをもとのデータフレームに追加する必要があります。
例えば0埋めした該当月の売上個数のデータは以下のように処理します。
sold_lag_0 = df_pivot.stack().reset_index().rename(columns={0:"売上個数0埋め"})
まず、.stack()
でpivotになっていたものを解消して縦方向のマルチインデックスで表現できます。
商品ID 店舗ID 年月
1000001 0 1801 6.0
1802 3.0
1803 1.0
1804 2.0
1806 4.0
そうすれば、あとはマルチインデックスを解消して必要に応じてカラム名をいじれば結合できる形に整えることができました。
あとはカラム名を合わせてマージすれば完了です。
df_merge = pd.merge(df, sold_lag_0, how="right", on=['年月', '店舗ID', '商品ID'])
この時how="right"
にすれば右外部結合(右側にあるテーブルを基準として結合)することができて無事完了です。
年月 商品ID 店舗ID 売上個数 商品価格 売上個数0埋め
0 1801 1000001 0 6.0 420 6.0
1 1801 1000001 1 2.0 325 2.0
2 1801 1000001 2 1.0 420 1.0
あとはラグ特徴量も同様にして結合することができますし、価格についてはdf_pivot.interpolate("nearest", limit_direction='both', axis=1)
のようにして線形補完をすることもできます。
もっというと、ラグ特徴量同士を四則演算すればより多くの情報を引き出すことができるようになります。
今回学んだことをもとに次のコンペではもっといい成績を残せるように精進したいと思います。