1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[Kaggle] Predict Future Engineeringをやってみる

Last updated at Posted at 2020-05-09

はじめに

ゴールデンウィークに外出ができなかったので、kaggleの「Predict Future Sales」に挑戦してみました。

このお題は、日ごとに集計されたデータ(2013.1〜2015.10)から来月(2015.11)の商品の売上個数を予測するものです。

今回のデータの特徴は、

・とにかくデータが量が多い
・学習データとテストデータとでshopの種類とitemの種類が異なる
・学習データは日毎で、予測するのは月の売上個数なので、形式をテストデータに合わせる必要がある
・時系列の要素があるのでラグをとった特徴量を追加する必要がある

では、やっていきます。

今回はこれを参考にやっていきました。
kaggle: Feature engineering, xgboost

ライブラリのインポート

import copy
import numpy as np
import pandas as pd
from itertools import product

import matplotlib.pyplot as plt
import seaborn as sns

from lightgbm.sklearn import LGBMRegressor
from lightgbm import plot_importance
from sklearn.preprocessing import LabelEncoder

pd.set_option('display.max_rows', 100)

データの読み込み

day_train_data = pd.read_csv('path/sales_train.csv')
month_test_data = pd.read_csv('path/test.csv')
item_categories = pd.read_csv('path/item_categories.csv')
items = pd.read_csv('path/items.csv')
shops = pd.read_csv('path/shops.csv')

データの修正

今回はday_train_dataにある'date'は使わないので、先に削除していきます。

day_train_data.drop('date', axis=1, inplace=True)

big_category_idとsub_category_idの作成

item_categoriesの'item_category_name'を確認してみると、'-'の前後でカテゴリーが分かれています。

item_categories['item_category_name'].head()


# [出力結果]
# 0    PC - Гарнитуры/Наушники
# 1           Аксессуары - PS2
# 2           Аксессуары - PS3
# 3           Аксессуары - PS4
# 4           Аксессуары - PSP
# Name: item_category_name, dtype: object

これを分解してlabel encodingしていきます。また、同じような名前がついたものもあるので、修正していきます。

item_categories['split_name'] = item_categories['item_category_name'].str.split('-')
item_categories['big_category_name'] =\
     item_categories['split_name'].map(lambda x: x[0].strip())
item_categories['sub_category_name'] =\
     tem_categories['split_name'].map(lambda x: x[1].strip() if len(x) > 1 else x[0].strip())

item_categories.loc[item_categories['big_category_name'] == 'Чистые носители(штучные)', 'big_category_name'] = 'Чистые носители (шпиль)'

# label encoding
item_categories['big_category_id'] = LabelEncoder().fit_transform(item_categories['big_category_name'])
item_categories['sub_category_id'] = LabelEncoder().fit_transform(item_categories['sub_category_name'])

# データの整理
item_categories = item_categories[['item_category_id', 'big_category_id', 'sub_category_id']]

###city_idの作成と修正
カテゴリーと同様に、shopsの'shop_name'から都市の情報が得られるので、変数にしていきます。
ついでに、入力ミスもあるので、それも修正していきます。

shops.loc[shops['shop_name'] == 'Сергиев Посад ТЦ ""', 'shop_name'] = 'СергиевПосад ТЦ ""'

shops['city_name'] = shops['shop_name'].map(lambda x: x.split(' ')[0])
shops.loc[shops['city_name'] == '!Якутск', 'city_name'] = 'Якутск'

# label encoding
shops['city_id'] = LabelEncoder().fit_transform(shops['city_name'])

# データの整理
shops = shops[['shop_id', 'city_id']]

###shop_idの修正
'shop_name'を見てみると、同じような名前になっています。kaggleのNotebooksを確認すると、どうやら同じっぽいので、修正していきます。これ以外にも後何個か存在します。

shops[(shops['shop_id'] == 0) | (shops['shop_id'] == 57)]

[出力結果]

shop_id shop_name
0 !Якутск Орджоникидзе, 56 фран 0
57 Якутск Орджоникидзе, 56 57

修正するファイルは、day_train_dataとmonth_test_dataです。今回のプログラムでは、shopsの方はいじらない方がいいと思います。(後でマージとかをやるので、データ量地獄に陥ります。)

# shop_idの修正
day_train_data.loc[day_train_data['shop_id'] == 0, 'shop_id'] = 57
month_test_data.loc[month_test_data['shop_id'] == 0, 'shop_id'] = 57

day_train_data.loc[day_train_data['shop_id'] == 1, 'shop_id'] = 58
month_test_data.loc[month_test_data['shop_id'] == 1, 'shop_id'] = 58

day_train_data.loc[day_train_data['shop_id'] == 10, 'shop_id'] = 11
month_test_data.loc[month_test_data['shop_id'] == 10, 'shop_id'] = 11

###item_priceとitem_cnt_dayの修正
item_priceとitem_cnt_dayで大きく値が外れているデータを削除していきます。
また、item_priceにマイナスの値があるので、同じ時期に同じ場所で売られているアイテムの中央値で修正をしていきます。

# 外れ値の削除
day_train_data.drop(day_train_data[day_train_data['item_price'] >= 100000].index, inplace=True)
day_train_data.drop(day_train_data[day_train_data['item_cnt_day'] > 1000].index, inplace=True)

# item_priceのエラー値を修正
median = day_train_data[(day_train_data['date_block_num'] == 4) & (day_train_data['shop_id'] == 32) & (day_train_data['item_id'] == 2973) & (day_train_data['item_price'] >= 0)]['item_price'].median()
day_train_data.loc[day_train_data['item_price'] < 0, 'item_price'] = median

データセットの作成

データセットの作成を行なっていきたいと思います。今回作成するデータセットは、day_train_dataとmonth_test_dataを合体させた不完全なパネルデータ??的な感じでです。また、予測するのは、month_test_dataにあるshopごとのitemなので、month_test_dataをdate_block_numの分だけコピーして縦にくっつけるような完全なパネルデータを作成するのもありだと思います。
ただ、その方法をやると、day_train_dataの一部が無駄になってしまうので、今回は前者の方でやっていきたいと思います。

# data_setの作成
data_set = []
cols = ['date_block_num', 'shop_id', 'item_id']
for date_num in range(34):
    sales = day_train_data[day_train_data['date_block_num'] == date_num]
    data_set.append(np.array(list(product([date_num],
                                          sales['shop_id'].unique(),
                                          sales['item_id'].unique()))))

data_set = pd.DataFrame(np.vstack(data_set), columns=cols)
data_set.sort_values(cols, inplace=True)
data_set = data_set.reset_index(drop=True)

# month_test_dataのmerge
month_test_data['date_block_num'] = 34
month_test_data['date_block_num'] = month_test_data['date_block_num']
month_test_data['shop_id'] = month_test_data['shop_id']
month_test_data['item_id'] = month_test_data['item_id']

data_set = pd.concat([data_set, month_test_data], ignore_index=True, sort=False, keys=cols)

# data_set = data_set + shops [city_id]
data_set = pd.merge(data_set, shops, on=['shop_id'], how='left')

# data_set = data_set + items [item_category_id]
data_set = pd.merge(data_set, items[['item_id', 'item_category_id']], on=['item_id'], how='left')

# data_set = data_set + item_categories [big_category_id, sub_category_id]
data_set = pd.merge(data_set, item_categories, on=['item_category_id'], how='left')

item_cnt_monthの作成

day_train_dataから月の売上個数を作成していきたいと思います。
この変数が目的変数となります。kaggleを見てみると、この変数をクリップした方が予測精度が上がるらしいです。

# item_sales_dayの作成
day_train_data['item_sales_day'] = day_train_data['item_price'] * day_train_data['item_cnt_day']

# item_cnt_monthの作成とmerge
merge_data = day_train_data.groupby(['date_block_num', 'shop_id', 'item_id'], as_index=False)['item_cnt_day'].agg('sum').rename(columns={'item_cnt_day': 'item_cnt_month'})

data_set = pd.merge(data_set, merge_data, on=cols, how='left')
data_set['item_cnt_month'] = data_set['item_cnt_month'].fillna(0).clip(0, 20)

月ごとのitem_cnt_monthをグラフに出してみます。

plot_data = day_train_data.groupby(['date_block_num'], as_index=False)['item_cnt_day'].agg('sum').rename(columns={'item_cnt_day': 'item_cnt_month'})

plt.figure(figsize=(20, 10))
sns.lineplot(data=plot_data, x='date_block_num', y='item_cnt_month')

date_cnt_month.png

年々、減少傾向にあり、毎年12月に同じような感じで売上個数が上がっていて、周期性が確認できます。
こういうところを変数として入れてあげると精度が上がりそうです。
変動分解でもっと細かくみていきたいと思います。

import statsmodels.api as sm
res = sm.tsa.seasonal_decompose(plot_data['item_cnt_month'], freq=12)

plt.subplots_adjust(hspace=0.3)
plt.figure(figsize=(15, 9))
plt.subplot(411)
plt.plot(res.observed, lw=.6, c='darkblue')
plt.title('observed')
plt.subplot(412)
plt.plot(res.trend, lw=.6, c='indianred')
plt.title('trend')
plt.subplot(413)
plt.plot(res.seasonal, lw=.6, c='indianred')
plt.title('seasonal')
plt.subplot(414)
plt.plot(res.resid, lw=.6, c='indianred')
plt.title('residual')

ここで一旦、データをキャストしていきます。

data_set.fillna(0, inplace=True)
data_set['date_block_num'] = data_set['date_block_num'].astype(np.uint8)
data_set['shop_id'] = data_set['shop_id'].astype(np.uint8)
data_set['item_id'] = data_set['item_id'].astype(np.uint16)
data_set['item_cnt_month'] = data_set['item_cnt_month'].astype(np.float16)
data_set['ID'] = data_set['ID'].astype(np.uint32)
data_set['city_id'] = data_set['city_id'].astype(np.uint8)
data_set['item_category_id'] = data_set['item_category_id'].astype(np.uint8)
data_set['big_category_id'] = data_set['big_category_id'].astype(np.uint8)
data_set['sub_category_id'] = data_set['sub_category_id'].astype(np.uint8)

residual.png

これを見ると、傾向や季節性があるのがよく分かりますね。

Feature Engineering

この関数は今回たくさん使っていきますが、一時的にメモリをたくさん使うので注意です。

def feature_lags(df, lags, lag_col):
    tmp = df[['date_block_num', 'shop_id', 'item_id', lag_col]]
    for lag in lags:
        shifted = tmp.copy()
        
        shifted.columns = ['date_block_num', 'shop_id', 'item_id', str(lag_col) + '_lag_' + str(lag)]
        
        shifted['date_block_num'] += lag
        shifted = shifted[shifted['date_block_num'] <= 34]
        
        df = pd.merge(df, shifted, on=['date_block_num', 'shop_id', 'item_id'], how='left', copy=False)

    return df

item_cnt_monthのfeature engineering

#target_lags
data_set = feature_lags(df=data_set, lags=[1, 2, 3, 6, 12], lag_col='item_cnt_month')


# date_avg_item_cnt
date_avg_item_cnt = data_set.groupby(['date_block_num'], as_index=False)['item_cnt_month'].agg('mean').rename(columns={'item_cnt_month': 'date_avg_item_cnt'})

data_set = pd.merge(data_set, date_avg_item_cnt, on=['date_block_num'], how='left')

data_set = feature_lags(df=data_set, lags=[1], lag_col='date_avg_item_cnt')
data_set.drop(['date_avg_item_cnt'], axis=1, inplace=True)


# date_item_avg_item_cnt
date_item_avg_item_cnt = data_set.groupby(['date_block_num', 'item_id'], as_index=False)['item_cnt_month'].agg('mean').rename(columns={'item_cnt_month': 'date_item_avg_item_cnt'})

data_set = pd.merge(data_set, date_item_avg_item_cnt, on=['date_block_num', 'item_id'], how='left')

data_set = feature_lags(df=data_set, lags=[1], lag_col='date_item_avg_item_cnt')
data_set.drop(['date_item_avg_item_cnt'], axis=1, inplace=True)


# date_shop_avg_item_cnt
date_shop_avg_item_cnt = data_set.groupby(['date_block_num', 'shop_id'], as_index=False)['item_cnt_month'].agg('mean').rename(columns={'item_cnt_month': 'date_shop_avg_item_cnt'})

data_set = pd.merge(data_set, date_shop_avg_item_cnt, on=['date_block_num', 'shop_id'], how='left')

data_set = feature_lags(df=data_set, lags=[1], lag_col='date_shop_avg_item_cnt')
data_set.drop(['date_shop_avg_item_cnt'], axis=1, inplace=True)


# date_category_avg_item_cnt
date_category_avg_item_cnt = data_set.groupby(['date_block_num', 'item_category_id'], as_index=False)['item_cnt_month'].agg('mean').rename(columns={'item_cnt_month': 'date_category_avg_item_cnt'})

data_set = pd.merge(data_set, date_category_avg_item_cnt, on=['date_block_num', 'item_category_id'], how='left')

data_set = feature_lags(df=data_set, lags=[1], lag_col='date_category_avg_item_cnt')
data_set.drop(['date_category_avg_item_cnt'], axis=1, inplace=True)


# date_shop_category_avg_item_cnt
date_shop_category_avg_item_cnt = data_set.groupby(['date_block_num', 'shop_id', 'item_category_id'], as_index=False)['item_cnt_month'].agg('mean').rename(columns={'item_cnt_month': 'date_shop_category_avg_item_cnt'})

data_set = pd.merge(data_set, date_shop_category_avg_item_cnt, on=['date_block_num', 'shop_id', 'item_category_id'], how='left')

data_set = feature_lags(df=data_set, lags=[1], lag_col='date_shop_category_avg_item_cnt')
data_set.drop(['date_shop_category_avg_item_cnt'], axis=1, inplace=True)


# date_shop_bigcat_avg_item_cnt
date_shop_bigcat_avg_item_cnt = data_set.groupby(['date_block_num', 'shop_id', 'big_category_id'], as_index=False)['item_cnt_month'].agg('mean').rename(columns={'item_cnt_month': 'date_shop_bigcat_avg_item_cnt'})

data_set = pd.merge(data_set, date_shop_bigcat_avg_item_cnt, on=['date_block_num', 'shop_id', 'big_category_id'], how='left')

data_set = feature_lags(df=data_set, lags=[1], lag_col='date_shop_bigcat_avg_item_cnt')
data_set.drop(['date_shop_bigcat_avg_item_cnt'], axis=1, inplace=True)


# date_shop_subcat_avg_item_cnt
date_shop_subcat_avg_item_cnt = data_set.groupby(['date_block_num', 'shop_id', 'sub_category_id'], as_index=False)['item_cnt_month'].agg('mean').rename(columns={'item_cnt_month': 'date_shop_subcat_avg_item_cnt'})

data_set = pd.merge(data_set, date_shop_subcat_avg_item_cnt, on=['date_block_num', 'shop_id', 'sub_category_id'], how='left')

data_set = feature_lags(df=data_set, lags=[1], lag_col='date_shop_subcat_avg_item_cnt')
data_set.drop(['date_shop_subcat_avg_item_cnt'], axis=1, inplace=True)


# date_bigcat_avg_item_cnt
date_bigcat_avg_item_cnt = data_set.groupby(['date_block_num', 'big_category_id'], as_index=False)['item_cnt_month'].agg('mean').rename(columns={'item_cnt_month': 'date_bigcat_avg_item_cnt'})

data_set = pd.merge(data_set, date_bigcat_avg_item_cnt, on=['date_block_num', 'big_category_id'], how='left')

data_set = feature_lags(df=data_set, lags=[1], lag_col='date_bigcat_avg_item_cnt')
data_set.drop(['date_bigcat_avg_item_cnt'], axis=1, inplace=True)


# date_subcat_avg_item_cnt
date_subcat_avg_item_cnt = data_set.groupby(['date_block_num', 'sub_category_id'], as_index=False)['item_cnt_month'].agg('mean').rename(columns={'item_cnt_month': 'date_subcat_avg_item_cnt'})

data_set = pd.merge(data_set, date_subcat_avg_item_cnt, on=['date_block_num', 'sub_category_id'], how='left')

data_set = feature_lags(df=data_set, lags=[1], lag_col='date_subcat_avg_item_cnt')
data_set.drop(['date_subcat_avg_item_cnt'], axis=1, inplace=True)


# date_city_avg_item_cnt
date_city_avg_item_cnt = data_set.groupby(['date_block_num', 'city_id'], as_index=False)['item_cnt_month'].agg('mean').rename(columns={'item_cnt_month': 'date_city_avg_item_cnt'})

data_set = pd.merge(data_set, date_city_avg_item_cnt, on=['date_block_num', 'city_id'], how='left')

data_set = feature_lags(df=data_set, lags=[1], lag_col='date_city_avg_item_cnt')
data_set.drop(['date_city_avg_item_cnt'], axis=1, inplace=True)


# date_item_city_avg_item_cnt
date_item_city_avg_item_cnt = data_set.groupby(['date_block_num', 'item_id', 'city_id'], as_index=False)['item_cnt_month'].agg('mean').rename(columns={'item_cnt_month': 'date_item_city_avg_item_cnt'})

data_set = pd.merge(data_set, date_item_city_avg_item_cnt, on=['date_block_num', 'item_id', 'city_id'], how='left')

data_set = feature_lags(df=data_set, lags=[1], lag_col='date_item_city_avg_item_cnt')
data_set.drop(['date_item_city_avg_item_cnt'], axis=1, inplace=True)

item_salesのfature engineering

# item_avg_item_price
item_avg_item_price = day_train_data.groupby(['item_id'], as_index=False)['item_price'].agg('mean').rename(columns={'item_price': 'item_avg_item_price'})

data_set = pd.merge(data_set, item_avg_item_price, on=['item_id'], how='left')


# date_item_avg_item_price
date_item_avg_item_price = day_train_data.groupby(['date_block_num', 'item_id'], as_index=False)['item_price'].agg('mean').rename(columns={'item_price': 'date_item_avg_item_price'})

data_set = pd.merge(data_set, date_item_avg_item_price, on=['date_block_num', 'item_id'], how='left')


lags = [1, 2, 3, 4, 5, 6]
data_set = feature_lags(df=data_set, lags=lags, lag_col='date_item_avg_item_price')

for lag in lags:
    data_set['delta_price_lag_' + str(lag)]  = (data_set['date_item_avg_item_price_lag_' + str(lag)] - data_set['item_avg_item_price'] / data_set['item_avg_item_price'])

def select_trend(row):
    for lag in lags:
        if row['delta_price_lag_' + str(lag)]:
            return row['delta_price_lag_' + str(lag)]
    return 0    

data_set['delta_price_lag'] = data_set.apply(select_trend, axis=1)
data_set['delta_price_lag'].fillna(0, inplace=True)


# 使わないデータの削除
features_to_drop = ['item_avg_item_price', 'date_item_avg_item_price']
for lag in lags:
    features_to_drop += ['date_item_avg_item_price_lag_' + str(lag)]
    features_to_drop += ['delta_price_lag_' + str(lag)]
    
data_set.drop(features_to_drop, axis=1, inplace=True)

item_sales_dayのfeature engineering

date_shop_sales = day_train_data.groupby(['date_block_num', 'shop_id'], as_index=False)['item_sales_day'].agg('sum').rename(columns={'item_sales_day': 'date_shop_sales'})

data_set = pd.merge(data_set, date_shop_sales, on=['date_block_num', 'shop_id'], how='left')

shop_avg_sales = day_train_data.groupby(['shop_id'], as_index=False)['item_sales_day'].agg('mean').rename(columns={'item_sales_day': 'shop_avg_sales'})

data_set = pd.merge(data_set, shop_avg_sales, on=['shop_id'], how='left')

data_set['delta_sales'] = (data_set['date_shop_sales'] - data_set['shop_avg_sales']) / data_set['shop_avg_sales']
data_set = feature_lags(df=data_set, lags=[1], lag_col='delta_sales')
data_set.drop(['date_shop_sales', 'shop_avg_sales', 'delta_sales'], axis=1, inplace=True)

月データと月の日数

data_set['month'] = data_set['date_block_num'] % 12

days = pd.Series([31,28,31,30,31,30,31,31,30,31,30,31])
data_set['day'] = data_set['month'].map(days)
data_set['day'] = data_set['day'].astype(np.uint8)

train_dataとtest_dataの作成

data_setからデータを分ける前にmergeによるNaNに0を入れておきます。

data_set.fillna(0, inplace=True)

これでdata_setの完成です。
data_setの情報を載せておきます。
これをキャストしていないと倍以上のデータ量になっていると思います。

data_set.info()

""" out
 <class 'pandas.core.frame.DataFrame'>
Int64Index: 11128004 entries, 0 to 11128003
Data columns (total 29 columns):
date_block_num                           uint8
shop_id                                  uint8
item_id                                  uint16
item_cnt_month                           float16
ID                                       uint32
city_id                                  uint8
item_category_id                         uint8
big_category_id                          uint8
sub_category_id                          uint8
item_cnt_month_lag_1                     float16
item_cnt_month_lag_2                     float16
item_cnt_month_lag_3                     float16
item_cnt_month_lag_6                     float16
item_cnt_month_lag_12                    float16
date_avg_item_cnt_lag_1                  float16
date_item_avg_item_cnt_lag_1             float16
date_shop_avg_item_cnt_lag_1             float16
date_category_avg_item_cnt_lag_1         float16
date_shop_category_avg_item_cnt_lag_1    float16
date_shop_bigcat_avg_item_cnt_lag_1      float16
date_shop_subcat_avg_item_cnt_lag_1      float16
date_bigcat_avg_item_cnt_lag_1           float16
date_subcat_avg_item_cnt_lag_1           float16
date_city_avg_item_cnt_lag_1             float16
date_item_city_avg_item_cnt_lag_1        float16
delta_price_lag                          float64
delta_sales_lag_1                        float64
month                                    uint8
day                                      uint8
dtypes: float16(17), float64(2), uint16(1), uint32(1), uint8(8)
memory usage: 764.1 MB
"""

次にtrain_dataとtest_dataを作成していきます。

target_col = 'item_cnt_month'
exclude_cols = ['ID', target_col]

feature_cols = []
for col in train_data.columns:
    if col not in exclude_cols:
        feature_cols.append(col)

x_train = train_data[train_data['date_block_num'] <= 32][feature_cols]
y_train = train_data[train_data['date_block_num'] <= 32][target_col]
x_val = train_data[train_data['date_block_num'] == 33][feature_cols]
y_val = train_data[train_data['date_block_num'] == 33][target_col]
x_test = test_data[feature_cols]

LightGBMによる学習

まずは、チューニングさせずにデフォルトでやっていきたいと思います。

ts = time.time()

gbm = LGBMRegressor(n_jobs=-1)
gbm.fit(x_train, y_train,
       eval_metric='rmse',
       eval_set=(x_val, y_val))

time.time() - ts

# rmse: 0.948226
# time:23.153272

チューニングなしだと0.948226でした。時間も30秒以内でランダムフォレストとかよりもだいぶ早く学習してくれました。

次に、max_depthとn_estimatorsを変えてやっていきたいと思います。

ts = time.time()

gbm = LGBMRegressor(n_estimators=10000, max_depth=10, n_jobs=-1)
gbm.fit(x_train, y_train,
        early_stopping_rounds=20,
        eval_metric='rmse',
        eval_set=(x_val, y_val),
        verbose=1)

time.time() - ts

# Early stopping, best iteration is: [105] valid_0's rmse: 0.943153
# time30.228255

n_estimatorsが105の時がベストスコアとなりました。
あんまりrmseに変化はないですね。

feature_importancesを可視化してみます。
date_item_avg_item_cnt_lag_1が一番効いてますね。

feature_importances.png

このモデルでpredictした結果をkaggleに提出しました。

結果は、0.96079でした。

終わりに

今回は、kaggleのNotebooksを参考にデータを作り予測してみました。

まだ、ハイパーパラメーターのチューニングをしっかりやっていないので、
次回は、optunaというライブラリのLightGBM lunarでチューニングをしていきたいと思います。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?