###はじめに
Aidemy Premium Planを受講してできるようになったことを紹介したいと思います。
プログラミングに関心を持ち始めた方の参考になればと思い、ブログを書きました。
全くの初心者がどのくらいできるようになるのかの参考になれば嬉しいです。
ただ、実際のコードを書いたことがない方には、自分の書いたコードの詳細を理解するのは
難しいと思うので、始めようと思っている方はこういうこともできるのかという
なんとなくのイメージをついていただければ幸いです。
###自己紹介
自動車部品メーカーのエンジニアをしてます。
(新入社員から今の会社に勤めており入社8年目になります。)
業務では必要なかったのですが、将来的にプログラミングは必須のスキルになると考え、
Python言語での機械学習の勉強を始めました。
###プログラミングの学習経歴
2021年4月〜 ProgateでプログラミングのWeb開発を学び修了書を受領
2021年5月〜Aidemy Premium Planでデータ分析6ヶ月コースを受講しました。
###学習内容(Aidemy)
・Python入門
・Numpy基礎
・Pandas基礎
・Matplotlib基礎
・データクレンジング
・スクレイピング
・機械学習概論
・機械学習におけるデータ前処理
・教師あり学習(回帰・分類)
・教師なし学習
・自然言語処理基礎
・感情分析/株価予測
・時系列データ解析
・ディープラーニング基礎
・タイタニック(kaggleのコンペ)
###学習の振り返り
上記Aidemyの講座は2ヶ月くらいで完了させました。実際にAidemyで学んだコードをKaggleのデータを使って計算させたりすることでよりイメージをつけることができました。3ヶ月目からは、Kaggleの初心者コンペやSignateのテーブルコンペに参加して実際にデータ分析スキルを実践的に向上させることができるようになりました。まだまだ、未熟ですが、Aidemyの講座で自走できるレベルにはなれたと思います。
###Aidemy後の学習内容
経済産業省主催のAIQUEST2021に参加できましたのでこちらを学んでいきたいと思います。
こちらは機械学習の基礎を学ぶのではなく、実際にAIを企業に実装することを実践で学んで行くという講座になっております。既存製品✖️AI✖️ビジネスの視点が製造業で働く私には重要であると考えているので、ビジネス視点からのAIというところも学びたいと思い応募し、参加できました。
他の方は、機械学習を用いたアートとかもされていて非常に面白そうだなと思ったので、今後こういうこともやってみたいなと思います。(最初はやってみようと思ったのですが、前処理が非常に大変で、前処理のスキルを学ぶよりは機械学習のスキルに焦点を絞って学びたいなというところでデータが揃っているKaggleのコンペに参加することをしていました。)
9月からは、AI QUEST2021で、動体検知を用いた、図面の加工形状の分類をやっています。少しずつですが画像認識についての学習も進んでいます。
また、実業務においては、下記書籍のベイズ最適化を用いた実験計画法を実装して開発業務を効率化することができました。プログラミングが扱えることになったことで、自分のできることの領域が非常に広がったと思います。本当に学んで正解だったと実感しています。
###Aidemy Premium Planの講座について感想
Aidemy Premium Planは、Pythonでの機械学習のチュートリアルとして、
プログラミングをやったことがない人が初めてやるには非常に良い講座でした。
ただ、自分的には、Aidemy Premium Planは3ヶ月で十分だったかなと思います。
ゲームでもそうですがチュートリアルやってると、色々自分でやってみたいことが出てきて、
そのタイミングが自分的には3ヶ月目頃でした。
自分はコロナ下で暇があったので良かったですが、
プライベートの時間が取りにくい方は6ヶ月はあった方が良いと思います。
###環境
Kaggle Kernelsで実施しました。
Python 3.7.10
##できるようになったこと
プログラミングが全くわからないというところから、
Aidemyの講座を受講して、自走できるレベルになれたのではと思ってます。
今回は、データ分析スキルの向上のためにKaggleの初心者コンペの
「Predict Future Sales」に挑戦しましたので、その内容を紹介させてください。
(Aidemyを受講してから4ヶ月目の取り組みになります。)
あくまで、初心者が勉強してどのくらいできるようになったのかという紹介になりますので、
より良い精度を求めたいのであれば、Kaggle内のコミュニティを確認してください。
Kaggleとは、世界中の機械学習・データサイエンスに携わっている人が集まるコミュニティです。その中のコンペに参加し、与えられたデータから機械学習で予測モデルを作って、その精度を競い合うことで、自身のデータ分析スキルを向上させることができます。
Predict Future Salesのコンペは、Kaggleで皆が最初に挑戦するタイタニック号の生存者予測の次のレベルのテーブルコンペになります。
###結論
最初に結論書いておきます。後は、詳細になりますので、よければ見てください。
・順位は12000位中5100位くらいでまずまずでした。
・タイタニック号と違って、与えられたトレインデータを使えば良いのではなく、自分でトレインデータを作り直す必要があるのでよりデータを理解するスキルが上がったと思います。
・時系列データを扱うためデータをリークさせないで特徴量を作るスキルが上がったと思います。
・KaggleのKernelで実施したので、メモリ対策に苦労しました。この辺りも勉強になりました。
Kaggleのコンペにはこれからも参加してAIスキルを向上させたいと思います。
###詳細
まず最初にKaggleのkernelを開く前にやるべきこととして OverviewとRulesの確認しました。
####Overview
1C Companyというロシアの会社の与えられたある期間の毎日の売り上げデータに対して、
与えられたデータから翌月の販売数を予測するコンペとなります。
(各店舗の各商品の販売数をそれぞれ予測します。)
####Evaluation
評価は、予測値と答えの誤差をRMSE(root mean squared error)で計算し、
その値が小さい方が精度が良いというコンペになります。
このRMSEは、予測値と答えの小さなズレが複数個あるより、1つの大きなずれに対して罰則が大きくなる評価になります。
また、このコンペでの注意点ですが、True target values are clipped into [0,20] rangeとさらっとかかれており、予測値は0〜20の値になります。これを忘れて計算すると当然ですが全く精度が出なくなります。
このあたりが、最初にコンペのOverviewとRuleを読めという理由の1つですね。
(最初、これを無視して計算してしまい、全然精度出ませんでした)
####進め方
・Simple EDA
・外れ値の処理
・Shop_idとShop_listの処理
・item_idとitem_catagoryの処理
・トレインデータの再構築と1つのテーブルデータへ統合
・簡単な特徴量作成
・ラグ特徴量の作成とTargetエンコーディング
・少し発展した特徴量の作成
・トレインデータ・テストデータ・バリデーションデータの作成
・モデルの作成
・結果に対する考察
では、データの読み込みから進めていきます。
import pandas as pd
import numpy as np
import gc
%matplotlib inline
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import seaborn as sns
# データの読み込み
item = pd.read_csv('/kaggle/input/competitive-data-science-predict-future-sales/items.csv')
sample_submission = pd.read_csv('/kaggle/input/competitive-data-science-predict-future-sales/sample_submission.csv')
item_categories = pd.read_csv('/kaggle/input/competitive-data-science-predict-future-sales/item_categories.csv')
train_df = pd.read_csv('/kaggle/input/competitive-data-science-predict-future-sales/sales_train.csv')
shop_list = pd.read_csv('/kaggle/input/competitive-data-science-predict-future-sales/shops.csv')
test_df = pd.read_csv('/kaggle/input/competitive-data-science-predict-future-sales/test.csv')
####Simple EDA
データを確認してみます。
train_df.head()
OUTPUT:
date date_block_num shop_id item_id item_price item_cnt_day
0 02.01.2013 0 59 22154 999.00 1.0
1 03.01.2013 0 25 2552 899.00 1.0
2 05.01.2013 0 25 2552 899.00 -1.0
3 06.01.2013 0 25 2554 1709.05 1.0
4 15.01.2013 0 25 2555 1099.00 1.0
各店の各アイテムの日々の販売数データになっています。
続いて、テストデータを見てみます。
test_df.head()
OUTPUT:
ID shop_id item_id
0 0 5 5037
1 1 5 5320
2 2 5 5233
3 3 5 5232
4 4 5 5268
テストデータは(トレインデータの)翌月の各店の各アイテムに対する月間販売数を予測しないといけないです。
さらにデータを見てみます。
fig, ax= plt.subplots(figsize=(18,4))
train_df['shop_id'].value_counts().plot(kind='bar', alpha=0.7, color=mcolors.TABLEAU_COLORS)
plt.title('Shop ID Values in Train Data')
fig, ax= plt.subplots(figsize=(18,4))
test_df['shop_id'].value_counts().plot(kind='bar', alpha=0.7, color=mcolors.TABLEAU_COLORS)
plt.title('Shop ID Values in Test Data')
Output:
トレインデータのショップごとのアイテムのカウント数とテストデータのショップごとのアイテムのカウント数の分布が全く異なります。
ここからわかることは、テストデータは、販売があったアイテム数をカウントしているので
売れる店と売れない店でデータに隔たりがありますが、テストデータは、各店で
均一に振り分けられたアイテム数を予測をすることになっています。
なので、答えの大半が0になりそうです。
続いて、データを日々の販売数から、月ごとの販売数にしてデータを見てみました。
トレインデータは、0-33ヶ月で、テストデータは34ヶ月目のデータになります。
sns.set()
fig, ax= plt.subplots(figsize=(18,4))
a = train_df.groupby('date_block_num').shop_id.count()
b = test_df
b['date_block_num'] = 34
c = b.groupby('date_block_num').shop_id.count()
d = pd.concat([a,c])
d.plot(kind='bar', alpha=0.7)
plt.title('Shop ID Values in Train Data & Test Data by date_block_num')
sns.set()
fig, ax= plt.subplots(figsize=(18,4))
train_df[train_df.date_block_num == 33]['shop_id'].value_counts().sort_index().plot(kind='bar', alpha=0.7, color=mcolors.TABLEAU_COLORS)
plt.title('Shop ID Values in Train Data on the month before the target month')
sns.set()
fig, ax= plt.subplots(figsize=(18,4))
test_df['shop_id'].value_counts(sort=False).plot(kind='bar', alpha=0.7, color=mcolors.TABLEAU_COLORS)
plt.title('Shop ID Values in Train Data on the target month')
一番上が、月ごとのshop_idごとのアイテム数のデータになります。真ん中は、テストデータの33ヶ月目のshop_idごとのアイテム数で、一番下は、トレインデータのshop_idごとのアイテム数になります。
明らかにテストデータ(34ヶ月目)のアイテム数が多いことがわかります。
今度は、トレインデータで月毎のトータルの販売数の推移と、テストデータの対象に絞った場合の月毎のトータル販売数の推移を見てみました。
a = train_df.groupby(['shop_id', 'item_id', 'date_block_num'])['item_cnt_day'].sum().rename('item_cnt_month').reset_index()
b = a.groupby('date_block_num').item_cnt_month.sum().reset_index()
plt.figure(figsize=(18,4))
plt.xticks(np.arange(0, 34, step=1))
sns.lineplot(data=b, x='date_block_num', y='item_cnt_month')
plt.title('Trainsition of item_cnt_month in Train Data')
Leak_test_df = pd.DataFrame()
for i in range(34):
c = test_df[['shop_id','item_id']]
c['date_block_num'] = i
Leak_test_df = pd.concat([Leak_test_df, c])
Leak_test_df = pd.merge(Leak_test_df, a, on=['shop_id', 'item_id', 'date_block_num'], how='left')
Leak_test_df['item_cnt_month'] = Leak_test_df['item_cnt_month'].clip(0,20)
d = Leak_test_df.groupby('date_block_num').item_cnt_month.sum().reset_index()
plt.figure(figsize=(18,4))
plt.xticks(np.arange(0, 34, step=1))
sns.lineplot(data=d, x='date_block_num', y='item_cnt_month')
plt.title('Trainsition of item_cnt_month in Test Data')
トレインデータで対象となる各月のトータル販売数の推移は、徐々に下がっているのですが、
テストデータの対象に絞った場合、各月のトータル販売数の推移は、徐々に上がっていることがわかります。
date_block_num = 11, 23は12月になるので、販売数が伸びています。
テストデータは、date_block_num = 34の11月ですが、少なくともdate_block_num = 33よりは大きい値になりそうです。
(DataLeakageのテクニックは今回使用しませんでしたが、テストデータの予測値の合計がdate_block_num = 33より小さいのであれば、今回RMSEで評価されるため補正で✖️1.1などしてみれば精度上がりそうな気がします。)
続いて、トレインデータとテストデータの各アイテムのヒストグラムを確認し、差がないかを確認しました。
plt.figure(figsize=(18,4))
sns.histplot(x=train_df.item_id)
plt.title('Histogram of item_id in Train Data')
plt.figure(figsize=(18,4))
a = train_df[train_df.date_block_num == 33]
plt.title('Histograms of item_id in Train Data of 33 & Test Data of 34')
sns.histplot(x=a.item_id, stat="density")
sns.histplot(x=test_df.item_id, stat="density", color='green', alpha=0.1)
下が、直前のdata_block_num=33の際のヒストグラムとテストデータdata_block_num=34の重ね合わせのヒストグラムになりますが、大きなズレはなさそうです。
####外れ値の処置
plt.figure(figsize=(18,4))
sns.boxplot(x=train_df.item_cnt_day)
plt.figure(figsize=(18,4))
sns.boxplot(x=train_df.item_price)
train_df = train_df[train_df.item_price <=100000]
train_df = train_df[train_df.item_cnt_day < 1000]
外れ値を削除しました。後でアイテムリストから確認しましたが、販売数の外れ値は、ビニール袋の数で、価格の外れ値は、数十人にソフトウェアを導入した額が1つに計上されていた気がします。
Shops Dataの処理
a = train_df['shop_id'].nunique()
b = test_df['shop_id'].nunique()
print('Shops in Train data: ', a)
print('Shops in Test data: ', b)
Output:
Shops in Train data: 60
Shops in Test data: 42
ショップ名を見ていきます。
shop_list.T
Output:
shop_name shop_id
0 !Якутск Орджоникидзе, 56 фран 0
1 !Якутск ТЦ "Центральный" фран 1
2 Адыгея ТЦ "Мега" 2
3 Балашиха ТРК "Октябрь-Киномир" 3
4 Волжский ТЦ "Волга Молл" 4
5 Вологда ТРЦ "Мармелад" 5
6 Воронеж (Плехановская, 13) 6
7 Воронеж ТРЦ "Максимир" 7
8 Воронеж ТРЦ Сити-Парк "Град" 8
9 Выездная Торговля 9
10 Жуковский ул. Чкалова 39м? 10
11 Жуковский ул. Чкалова 39м² 11
12 Интернет-магазин ЧС 12
13 Казань ТЦ "Бехетле" 13
14 Казань ТЦ "ПаркХаус" II 14
15 Калуга ТРЦ "XXI век" 15
16 Коломна ТЦ "Рио" 16
17 Красноярск ТЦ "Взлетка Плаза" 17
18 Красноярск ТЦ "Июнь" 18
19 Курск ТЦ "Пушкинский" 19
20 Москва "Распродажа" 20
21 Москва МТРЦ "Афи Молл" 21
22 Москва Магазин С21 22
23 Москва ТК "Буденовский" (пав.А2) 23
24 Москва ТК "Буденовский" (пав.К7) 24
25 Москва ТРК "Атриум" 25
26 Москва ТЦ "Ареал" (Беляево) 26
27 Москва ТЦ "МЕГА Белая Дача II" 27
28 Москва ТЦ "МЕГА Теплый Стан" II 28
29 Москва ТЦ "Новый век" (Новокосино) 29
30 Москва ТЦ "Перловский" 30
31 Москва ТЦ "Семеновский" 31
32 Москва ТЦ "Серебряный Дом" 32
33 Мытищи ТРК "XL-3" 33
34 Н.Новгород ТРЦ "РИО" 34
35 Н.Новгород ТРЦ "Фантастика" 35
36 Новосибирск ТРЦ "Галерея Новосибирск" 36
37 Новосибирск ТЦ "Мега" 37
38 Омск ТЦ "Мега" 38
39 РостовНаДону ТРК "Мегацентр Горизонт" 39
40 РостовНаДону ТРК "Мегацентр Горизонт" Островной 40
41 РостовНаДону ТЦ "Мега" 41
42 СПб ТК "Невский Центр" 42
43 СПб ТК "Сенная" 43
44 Самара ТЦ "Мелодия" 44
45 Самара ТЦ "ПаркХаус" 45
46 Сергиев Посад ТЦ "7Я" 46
47 Сургут ТРЦ "Сити Молл" 47
48 Томск ТРЦ "Изумрудный Город" 48
49 Тюмень ТРЦ "Кристалл" 49
50 Тюмень ТЦ "Гудвин" 50
51 Тюмень ТЦ "Зеленый Берег" 51
52 Уфа ТК "Центральный" 52
53 Уфа ТЦ "Семья" 2 53
54 Химки ТЦ "Мега" 54
55 Цифровой склад 1С-Онлайн 55
56 Чехов ТРЦ "Карнавал" 56
57 Якутск Орджоникидзе, 56 57
58 Якутск ТЦ "Центральный" 58
59 Ярославль ТЦ "Альтаир"
まず、最初の単語「Адыгея」は都市名を表記していました。
よくみるとショップ名の誤表記で分かれている番号があるので、統一します。
train_df.loc[train_df['shop_id'] == 0, 'shop_id'] = 57
test_df.loc[test_df['shop_id'] == 0, 'shop_id'] = 57
train_df.loc[train_df['shop_id'] == 1, 'shop_id'] = 58
test_df.loc[test_df['shop_id'] == 1, 'shop_id'] = 58
train_df.loc[train_df['shop_id'] == 10, 'shop_id'] = 11
test_df.loc[test_df['shop_id'] == 10, 'shop_id'] = 11
train_df.loc[train_df['shop_id'] == 40, 'shop_id'] = 39
test_df.loc[test_df['shop_id'] == 40, 'shop_id'] = 39
ショップ名から都市を割り出し、各都市の座標と州を設定しました。
from sklearn.preprocessing import LabelEncoder
# extract and encode cities
shop_list['city'] = shop_list['shop_name'].apply(lambda x: x.split()[0].lower())
shop_list.loc[shop_list.city == '!якутск', 'city'] = 'якутск'
shop_list['city_id'] = LabelEncoder().fit_transform(shop_list['city'])
# add coordinates of cities
coords = dict()
coords['якутск'] = (62.028098, 129.732555, 4)
coords['адыгея'] = (44.609764, 40.100516, 3)
coords['балашиха'] = (55.8094500, 37.9580600, 1)
coords['волжский'] = (53.4305800, 50.1190000, 3)
coords['вологда'] = (59.2239000, 39.8839800, 2)
coords['воронеж'] = (51.6720400, 39.1843000, 3)
coords['выездная'] = (0, 0, 0)
coords['жуковский'] = (55.5952800, 38.1202800, 1)
coords['интернет-магазин'] = (0, 0, 0)
coords['казань'] = (55.7887400, 49.1221400, 4)
coords['калуга'] = (54.5293000, 36.2754200, 4)
coords['коломна'] = (55.0794400, 38.7783300, 4)
coords['красноярск'] = (56.0183900, 92.8671700, 4)
coords['курск'] = (51.7373300, 36.1873500, 3)
coords['москва'] = (55.7522200, 37.6155600, 1)
coords['мытищи'] = (55.9116300, 37.7307600, 1)
coords['н.новгород'] = (56.3286700, 44.0020500, 4)
coords['новосибирск'] = (55.0415000, 82.9346000, 4)
coords['омск'] = (54.9924400, 73.3685900, 4)
coords['ростовнадону'] = (47.2313500, 39.7232800, 3)
coords['спб'] = (59.9386300, 30.3141300, 2)
coords['самара'] = (53.2000700, 50.1500000, 4)
coords['сергиев'] = (56.3000000, 38.1333300, 4)
coords['сургут'] = (61.2500000, 73.4166700, 4)
coords['томск'] = (56.4977100, 84.9743700, 4)
coords['тюмень'] = (57.1522200, 65.5272200, 4)
coords['уфа'] = (54.7430600, 55.9677900, 4)
coords['химки'] = (55.8970400, 37.4296900, 1)
coords['цифровой'] = (0, 0, 0)
coords['чехов'] = (55.1477000, 37.4772800, 4)
coords['ярославль'] = (57.6298700, 39.8736800, 2)
shop_list['city_coord_1'] = shop_list['city'].apply(lambda x: coords[x][0])
shop_list['city_coord_2'] = shop_list['city'].apply(lambda x: coords[x][1])
shop_list['country_part'] = shop_list['city'].apply(lambda x: coords[x][2])
shop_list = shop_list[['shop_id', 'city_id', 'city_coord_1', 'city_coord_2', 'country_part']]
####item_idとitem_catagoryの処理
続いて、商品(item)を処理していきます。
item.head()
Output:
item_name item_id item_category_id
0 ! ВО ВЛАСТИ НАВАЖДЕНИЯ (ПЛАСТ.) D 0 40
1 !ABBYY FineReader 12 Professional Edition Full... 1 76
2 ***В ЛУЧАХ СЛАВЫ (UNV) D 2 40
3 ***ГОЛУБАЯ ВОЛНА (Univ) D 3 40
4 ***КОРОБКА (СТЕКЛО) D 4
item.info()
Output:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 22170 entries, 0 to 22169
Data columns (total 3 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 item_name 22170 non-null object
1 item_id 22170 non-null int64
2 item_category_id 22170 non-null int64
dtypes: int64(2), object(1)
memory usage: 519.7+ KB
商品名に関しては、共通項を検討することが難しそうでしたので、今回は排除しました。
item = item.drop('item_name', axis=1)
続いて商品カテゴリー処理します。
item_categories.head()
商品カテゴリーに関しては、共通点が多く見られました。
例えば、「Аксессуары」はアクセサリーで、付属品を示しています。
item_category_name item_category_id
0 PC - Гарнитуры/Наушники 0
1 Аксессуары - PS2 1
2 Аксессуары - PS3 2
3 Аксессуары - PS4 3
4 Аксессуары - PSP 4
そこで、共通点に関するところでグルーピングしていきました。
item_categories['item_major_category_id'] = item_categories['item_category_name'].str.split(' ').map(lambda x: x[0])
from sklearn import preprocessing
le = preprocessing.LabelEncoder()
item_categories['item_major_category_id'] = le.fit_transform(item_categories['item_major_category_id'])
item_categories['item_sub_category_id'] = item_categories['item_category_name'].str.split('-').map(lambda x: x[1] if len(x) > 1 else x[0] )
item_categories['item_sub_category_id'] = le.fit_transform(item_categories['item_sub_category_id'])
item_categories = item_categories.drop('item_category_name', axis=1)
####トレインデータの再構築と1つのテーブルデータへ統合
まず、今あるトレインデータを加工せずに機械学習させ、提出しましたが、精度は低かったです。
これは、今回のトレインデータは販売されたものだけがカウントされているので、
販売されなかった際の0のデータがトレインデータに入っていないことが原因だと推測しました。
(テストデータも多くが0の値になりそうですし、)
そこで、トレインデータの再構築を行いました。
# Pattern 1
new_train_df_ver3 = pd.DataFrame()
for i in range(35):
c = test_df[['shop_id','item_id']]
c['date_block_num'] = i
new_train_df_ver3 = pd.concat([Leak_test_df, c])
del new_train_df_ver3
gc.collect()
パターン1では、テストデータの対象のshop_idとitem_idの組み合わせに注目して
トレインデータを再構築しました。
精度はある程度上がりましたが、そこまで良い結果は得られませんでした。
# Pattern 2
from itertools import product
index_cols_ver2 = ['shop_id', 'item_id']
a = []
cur_shops = train_df['shop_id'].unique()
cur_items = train_df['item_id'].unique()
a.append(np.array(list(product(*[cur_shops, cur_items])),dtype='int32'))
a = pd.DataFrame(np.vstack(a), columns = index_cols_ver2,dtype=np.int32)
new_train_df_ver2 = pd.DataFrame()
for block_num in range(34):
a['date_block_num'] = block_num
new_train_df_ver2 = pd.concat([new_train_df_ver2, a])
new_train_df_ver2['shop_id'] = new_train_df_ver2['shop_id'].astype('int8')
new_train_df_ver2['date_block_num'] = new_train_df_ver2['date_block_num'].astype('int8')
new_train_df_ver2.info()
del new_train_df_ver2
gc.collect()
パターン2では、トレインデータにある全てのshop_idとitem_idの組み合わせを作り、
それが35ヶ月間(0-34)あると考え、計算しようしました。
これは、データ数があまりにも大きくなってしまい、kaggleのKernelではメモリ不足になってしまったので、今回は諦めました。
# Pattern 3
from itertools import product
index_cols = ['shop_id', 'item_id', 'date_block_num']
new_train_df = []
for block_num in train_df['date_block_num'].unique():
cur_shops = train_df.loc[train_df['date_block_num'] == block_num, 'shop_id'].unique()
cur_items = train_df.loc[train_df['date_block_num'] == block_num, 'item_id'].unique()
new_train_df.append(np.array(list(product(*[cur_shops, cur_items, [block_num]])),dtype='int32'))
new_train_df = pd.DataFrame(np.vstack(new_train_df), columns = index_cols,dtype=np.int32)
パターン3で進めました。パターン3では、トレインデータの各月ごとで存在するshop_idとitem_idの組み合わせを全て考慮しました。
ここからは、再構築したトレインデータに、テストデータやitemsやitem_categoriesやshop_listのデータを結合させ、特徴量を作っていきました。
test_df['date_block_num'] = 34
test_df1 = test_df.drop('ID', axis=1)
all_data = pd.concat([new_train_df, test_df1])
a = train_df.groupby(['shop_id', 'item_id', 'date_block_num'])['item_cnt_day'].sum().rename('item_cnt_month').reset_index()
all_data = pd.merge(all_data, a, on=['shop_id','item_id','date_block_num'], how='left')
all_data.fillna(0, inplace=True)
all_data = pd.merge(all_data, item, on='item_id', how='left')
all_data = pd.merge(all_data, item_categories, on='item_category_id', how='left')
all_data = pd.merge(all_data, shop_list, on='shop_id', how='left')
all_data['month'] = all_data['date_block_num'] % 12
days = pd.Series([31,28,31,30,31,30,31,31,30,31,30,31])
all_data['days'] = all_data['month'].map(days)
all_data = all_data.astype('int16')
all_data['item_cnt_month'] = all_data['item_cnt_month'].clip(0,20)
del item
del item_categories
del shop_list
gc.collect()
####簡単な特徴量作成
まず、簡単な特徴量を作ってました。
2年目以降の12月前後の月では販売数が増えるので、それを考慮した特徴量を入れてみました。
後は、年数を特徴量にしてみました。
all_data['sales_season'] = 0
all_data.loc[all_data.date_block_num == 22, 'sales_season'] = 1
all_data.loc[all_data.date_block_num == 23, 'sales_season'] = 1
all_data.loc[all_data.date_block_num == 24, 'sales_season'] = 1
all_data.loc[all_data.date_block_num == 34, 'sales_season'] = 1
all_data['sales_season'] = all_data['sales_season'].astype('int8')
all_data['year'] = 14
all_data.loc[all_data.date_block_num / 12 < 1, 'year'] = 13
all_data.loc[all_data.date_block_num / 12 >=2, 'year'] = 15
all_data['year'] = all_data['year'].astype('int8')
####ラグ特徴量の作成とTargetエンコーディング
続いて時系列データでよく使うLag featureというテクニックを使いました。
def lag_features(data, months, lag_column):
for month in months:
data_shift = data[['date_block_num', 'shop_id', 'item_id', lag_column]].copy()
data_shift.columns = ['date_block_num', 'shop_id', 'item_id', lag_column+'_lag_'+ str(month)]
data_shift['date_block_num'] += month
data = pd.merge(data, data_shift, on=['date_block_num', 'shop_id', 'item_id'], how='left')
return data
all_data = lag_features(all_data, [1,2,3], 'item_cnt_month')
カテゴリカルデータをtarget encodingで色々特徴量を作っていきました。
また、データリークの対策としてLag featureでのtarget eoncodingを行いました。
a = all_data.groupby(['date_block_num']).item_cnt_month.sum().astype('int32').rename('total_cnt_month').reset_index()
all_data = pd.merge(all_data, a, on=['date_block_num'], how='left')
all_data = lag_features(all_data, [1, 2, 3], 'total_cnt_month')
all_data.drop('total_cnt_month', axis=1, inplace=True)
a = train_df.groupby(['shop_id', 'item_id', 'date_block_num']).item_price.mean().rename('mean_shop_price').reset_index()
all_data = pd.merge(all_data, a, on=['shop_id', 'item_id', 'date_block_num'], how='left')
all_data['mean_shop_price'] = all_data['mean_shop_price'].fillna(0).astype('float16')
a = train_df.groupby(['item_id', 'date_block_num']).item_price.mean().rename('mean_item_price').reset_index()
all_data = pd.merge(all_data, a, on=['item_id', 'date_block_num'], how='left')
all_data['mean_item_price'] = all_data['mean_item_price'].fillna(0).astype('float16')
all_data['item_shop_price_mean'] = (all_data['mean_shop_price'] - all_data['mean_item_price']) / all_data['mean_item_price']
all_data['item_shop_price_mean'].fillna(0, inplace=True)
all_data = lag_features(all_data, [1, 2, 3], 'item_shop_price_mean')
all_data.drop(['mean_shop_price', 'mean_item_price', 'item_shop_price_mean'], axis=1, inplace=True)
a = all_data.groupby(['date_block_num', 'item_id', 'city_id'])['item_cnt_month'].mean().astype('float16').rename('item_city_month_mean').reset_index()
all_data = pd.merge(all_data, a, on=['date_block_num', 'item_id', 'city_id'], how='left')
all_data = lag_features(all_data,[1,2,3], 'item_city_month_mean')
all_data.drop(['item_city_month_mean'], axis=1, inplace=True)
a = all_data.groupby(['date_block_num','item_id', 'shop_id'])['item_cnt_month'].mean().rename('item_shop_mean').reset_index()
all_data = pd.merge(all_data, a, on=['date_block_num','item_id', 'shop_id'], how='left')
all_data['item_shop_mean'] = all_data['item_shop_mean'].fillna(0).astype('float16')
all_data = lag_features(all_data, [1, 2, 3], 'item_shop_mean')
all_data.drop(['item_shop_mean'], axis=1, inplace=True)
all_data = all_data.fillna(0)
####少し発展した特徴量の作成
新たな特徴量として、そのitem_idが初めて販売された月かどうかを特徴量とし、また商品がそれまで売られていたかどうかを特徴量としました。また、それを元にラグ特徴量としました。
first_item_block = train_df.groupby(['item_id'])['date_block_num'].min().reset_index()
first_item_block['item_first_interaction'] = 1
first_shop_item_buy_block = train_df.groupby(['shop_id', 'item_id'])['date_block_num'].min().reset_index()
first_shop_item_buy_block['first_date_block_num'] = first_shop_item_buy_block['date_block_num']
all_data = pd.merge(all_data, first_item_block[['item_id', 'date_block_num', 'item_first_interaction']], on=['item_id', 'date_block_num'], how='left')
all_data = pd.merge(all_data, first_shop_item_buy_block[['item_id', 'shop_id', 'first_date_block_num']], on=['item_id', 'shop_id'], how='left')
all_data['first_date_block_num'].fillna(100, inplace=True)
all_data['shop_item_sold_before'] = (all_data['first_date_block_num'] < all_data['date_block_num']).astype('int8')
all_data.drop(['first_date_block_num'], axis=1, inplace=True)
all_data['item_first_interaction'].fillna(0, inplace=True)
all_data['shop_item_sold_before'].fillna(0, inplace=True)
a = all_data[all_data['item_first_interaction'] == 1].groupby(['date_block_num','item_category_id','shop_id'])['item_cnt_month'].mean().rename("new_item_shop_cat_mean").reset_index()
all_data = pd.merge(all_data, a, on=['date_block_num','item_category_id', 'shop_id'], how='left')
all_data['new_item_shop_cat_mean'] = all_data['new_item_shop_cat_mean'].fillna(0).astype('float16')
all_data = lag_features(all_data, [1, 2, 3], 'new_item_shop_cat_mean')
all_data.drop(['new_item_shop_cat_mean'], axis=1, inplace=True)
特徴量を作成が完了したので、データを確認しました。
all_data.info()
Output:
<class 'pandas.core.frame.DataFrame'>
Int64Index: 11056277 entries, 0 to 11056276
Data columns (total 35 columns):
# Column Dtype
--- ------ -----
0 shop_id int16
1 item_id int16
2 date_block_num int16
3 item_cnt_month int16
4 item_category_id int16
5 item_major_category_id int16
6 item_sub_category_id int16
7 city_id int16
8 city_coord_1 int16
9 city_coord_2 int16
10 country_part int16
11 month int16
12 days int16
13 sales_season int8
14 year int8
15 item_cnt_month_lag_1 float64
16 item_cnt_month_lag_2 float64
17 item_cnt_month_lag_3 float64
18 total_cnt_month_lag_1 float64
19 total_cnt_month_lag_2 float64
20 total_cnt_month_lag_3 float64
21 item_shop_price_mean_lag_1 float16
22 item_shop_price_mean_lag_2 float16
23 item_shop_price_mean_lag_3 float16
24 item_city_month_mean_lag_1 float16
25 item_city_month_mean_lag_2 float16
26 item_city_month_mean_lag_3 float16
27 item_shop_mean_lag_1 float16
28 item_shop_mean_lag_2 float16
29 item_shop_mean_lag_3 float16
30 item_first_interaction float64
31 shop_item_sold_before int8
32 new_item_shop_cat_mean_lag_1 float16
33 new_item_shop_cat_mean_lag_2 float16
34 new_item_shop_cat_mean_lag_3 float16
dtypes: float16(12), float64(7), int16(13), int8(3)
memory usage: 1.2 GB
一部float64だったりするので、メモリ削減しました。
all_data = all_data.fillna(0)
# Memory reduction
all_data['item_first_interaction'] = all_data['item_first_interaction'].astype('int8')
all_data.iloc[:, 15:21] = all_data.iloc[:, 15:21].astype('int32')
del train_df
del test_df
gc.collect()
####トレイン・テスト・バリデーションデータの作成
トレインデータとテストデータを再び分けます。
test = all_data[all_data.date_block_num == 34]
test = test.drop('item_cnt_month', axis=1)
new_train_df = all_data[all_data.date_block_num < 34]
lag featureを行いましたが、最初の3ヶ月間(date_block_num = 0 ~ 2)の lag featureが0になっているので、トレインデータから省きました。
From_Month = 3
new_train_df = new_train_df[new_train_df.date_block_num >= From_Month]
train = new_train_df.drop('item_cnt_month', axis=1)
y_train = new_train_df['item_cnt_month']
Validationとは、date_block_num = 33のデータとしました。
To_Month = 33
tr_X = train[train.date_block_num < To_Month]
val_X = train[train.date_block_num >= To_Month]
ntr_X = tr_X.shape[0]
tr_Y = y_train[:ntr_X]
val_Y = y_train[ntr_X:]
del all_data
del train
gc.collect()
####モデルの作成
勾配ブースティング木手法であるlightGBMを使用しました。
また、メトリックは、今回の評価と同じrmseを使用しました。
ここには記載しませんが、一応、ハイパーパラメータはOptunaを使用して最適化しました。
(どのパラメータが過学習するのか、どのパラメータが汎用化するのかはわかるのですが、
未だ、どの程度の値が経験的に妥当かはわかっていないです。)
import lightgbm as lgb
#import optuna.integration.lightgbm as lgb
from sklearn.metrics import r2_score, mean_squared_error
lgb_train =lgb.Dataset(tr_X, tr_Y)
lgb_eval = lgb.Dataset(val_X, val_Y)
params = {'bagging_fraction': 1.0,
'bagging_freq': 0,
'boosting_type': 'gbdt',
'feature_fraction': 0.48000000000000004,
'feature_pre_filter': False,
'lambda_l1': 9.870317725539854,
'lambda_l2': 3.75055716395215e-07,
'metric': 'rmse',
'min_child_samples': 20,
'num_leaves': 216,
'objective': 'rmse',
'learning_rate':0.005,
'seed': 57}
num_round = 1000
categorical_features = ['shop_id', 'item_id','city_id', 'item_category_id', 'item_major_category_id', 'item_sub_category_id',
'country_part', 'sales_season', 'item_first_interaction', 'shop_item_sold_before']
model1 = lgb.train(params, lgb_train, num_boost_round=num_round,
categorical_feature = categorical_features,
valid_names=['train', 'valid'], valid_sets=[lgb_train,lgb_eval], early_stopping_rounds =30)
va_pred = model1.predict(val_X)
score = np.sqrt(mean_squared_error(val_Y, va_pred))
print(score)
Early stopping, best iteration is:
[647] train's rmse: 0.754511 valid's rmse: 0.917492
0.9174921913985981
そして、提出しました。
sample_submission = pd.read_csv('/kaggle/input/competitive-data-science-predict-future-sales/sample_submission.csv')
LGBMpred = model1.predict(test)
LGBMpred = LGBMpred.clip(0, 20).astype('float32')
sample_submission['item_cnt_month'] = LGBMpred
sample_submission.to_csv('submission_a.csv', index=False)
せっかくなので、線形だったり、ニューラルネットワークも試してみました。
時系列データということもあり、アンサンブルは、簡単な加重平均で検討しました。
del model1
gc.collect()
線形モデルでは、標準化が必要になるので、標準化しました。
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
tr_X = scaler.fit_transform(tr_X)
val_X = scaler.transform(val_X)
test_X = scaler.transform(test)
del scaler
gc.collect()
ラッソ回帰を用いて計算しました。
from sklearn import linear_model
reg = linear_model.Lasso(alpha=0.1)
reg.fit(tr_X, tr_Y)
reg_pred = reg.predict(test)
va_pred = reg.predict(val_X)
score2 = np.sqrt(mean_squared_error(val_Y, va_pred))
print(f'rsme: {score2:.4f}')
Output:
rsme: 0.9844
LightGBMより精度悪そうですね。
reg_pred = reg_pred.clip(0,20).astype('float32')
del reg
gc.collect()
続いてニューラルネットワークも使ってみました。
from keras.layers import Dense, Dropout
from keras.models import Sequential
from sklearn.metrics import mean_squared_error
model2 = Sequential()
model2.add(Dense(256, activation='relu', input_shape=(tr_X.shape[1],)))
model2.add(Dropout(0.2))
model2.add(Dense(256, activation='relu'))
model2.add(Dropout(0.2))
model2.add(Dense(1))
model2.compile(loss='mse', optimizer='adam', metrics=['mean_squared_error'])
model2.summary()
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint, ReduceLROnPlateau
callbacks = [
EarlyStopping(patience=5, verbose=1),
ReduceLROnPlateau(factor=0.25, patience=2, min_lr=0.000001, verbose=1),
ModelCheckpoint('model.h5', verbose=1, save_best_only=True, save_weights_only=True)
]
model2.fit(tr_X, tr_Y, epochs=15, callbacks=callbacks, validation_data=(val_X, val_Y))
NN_predict = model2.predict(test)
va_pred = model2.predict(val_X)
score2 = np.sqrt(mean_squared_error(val_Y, va_pred))
print(f'rsme: {score2:.4f}')
Epoch 00011: val_loss did not improve from 0.88227
Epoch 00011: early stopping
rsme: 0.9408
Lightgbmより精度は下がりそうです。
NN_predict = NN_predict.clip(0,20).astype('float32')
NN_predict = NN_predict.ravel()
del model2
gc.collect()
精度は下がりそうですが、せっかくなので最後までアンサンブルをやってみました。
Ensemble = LGBMpred*0.8
Ensemble += NN_predict*0.1
Ensemble += reg_pred*0.1
sample_submission['item_cnt_month'] = Ensemble.clip(0, 20)
sample_submission.to_csv('submission_b.csv', index=False)
実際、Lightgbm単体の方が精度が良かったです。
順位は、12000人中5100人くらいになりました。
まずまずの結果を出すことができました。
####結果に対する考察
とりあえずでアンサンブルしてみたのですが、よくよくデータをみると恥ずかしながらニューラルネットワークのモデルが過学習していて、全く精度が出ないモデルになってました。
sample_submission['LGBMpred'] = LGBMpred
sample_submission['NN_pred'] = NN_pred
sample_submission['reg_pred'] = reg_pred
sample_submission.head()
Output:
ID item_cnt_month LGBMpred NN_pred reg_pred
0 0 0.885253 0.885253 0.173717 0.590238
1 1 0.074132 0.074132 0.030672 0.296229
2 2 1.401056 1.401056 0.173717 1.160684
3 3 0.200387 0.200387 0.173717 0.365134
4 4 0.899215 0.899215 0.031228 0.296229
グラフもするとこんな感じでした。
plt.figure(figsize=(18,8))
plt.scatter(sample_submission.ID, sample_submission.LGBMpred, s=10, c='b', marker='.', alpha=0.5)
plt.scatter(sample_submission.ID, sample_submission.reg_pred, s=10, c='r', marker='.', alpha=0.5)
plt.scatter(sample_submission.ID, sample_submission.NN_pred, s=10, c='yellow', marker='.', alpha=0.5)
Output:
ニューラルネットワークで計算したモデルが0に収束していました。なので、意味のない予測モデルになっていました。
また、後々考えたのですが、仮にこのニューラルネットワークの予測モデルがある程度精度が出ていたとしても、赤と青のプロットを見ると、加重平均のアンサンブルでは上手く行かないと思われます。
そう考えるに至った例を示します。
# 加重平均アンサンブルの例
a = pd.DataFrame()
a['x'] = np.arange(0, 101, 1)
a['y_1'] = a['x'] + np.random.rand(101) * 40 + 5
a['y_2'] = a['x'] + np.random.rand(101) * 40 + 5
a['y_3'] = a['x'] + np.random.rand(101) * 40 + 5
a['y_4'] = (a['y_1'] + a['y_2'] + a['y_3']) / 3
plt.figure()
plt.scatter(a.x, a.y_1, color='b', marker='.')
plt.scatter(a.x, a.y_2, color='b', marker='.', alpha=0.6)
plt.scatter(a.x, a.y_3, color='b', marker='.', alpha=0.3)
plt.scatter(a.x, a.y_4, color='r', marker='.')
plt.xlabel('x')
plt.ylabel('y')
Output:
赤のプロットが3つのプロットの平均の値を示してますが、平均化することで、予測モデルのばらつきを抑制することが可能になります。しかし、今回の予測は、各予測値の振れが大きく、正解のフレ(0-20)も大きくなると思われるので、目的のない機械学習同士のモデルのアンサンブルではあまり効果が期待できないと思われます。
そのため、今後の改良としては、アンサンブルは、特定の目的をもったモデルの組み合わせで検討したほうが良いと考えてます。例えば、売り上げ数が0かそうでないかの分類を行なって、0でない場合は、上記LightGBMを用いて予測するといったアンサンブルの方が精度が上がると思われるので試してみたいです。
そして、このコンペは、特徴量の生成が重要になると思うので、さらなる特徴量の追加も行っていきたいと思います。
特に、周期的な動きがあるので、移動平均を用いた特徴量の作成を検討したいです。
また、商品名の検討でfuzzywuzzyを検討されている方がいて、これも試してみたいです。
参考文献
1)kaggleで勝つデータ分析の技術
2)https://www.kaggle.com/dlarionov/feature-engineering-xgboost