〜初心者がPythonで小売データ分析をやってみた〜


はじめに

今回kaggleにあった過去のコンペ『Retail Data Analytics』からデータをダウンロードし、機械学習モデルの構築を行った。

自己紹介


・製造業の営業職に従事している社会人3年目
・プログラミング初心者

開発環境


・Macbook Air M1
・Google Colaboratory
・Chrome

目次

Step1:必要なモジュールのインポート
Step2:データを読み込み、前処理
Step3:データを可視化
Step4:学習用データとテスト用データに分ける
Step5:LightGBMモデルでの学習
振り返り

Step1:必要なモジュールのインポート

必要があればその都度、追加するようにする

%matplotlib inline #import  japanize_matplotlib
import matplotlib.pyplot as plt
from matplotlib_venn import venn2
import numpy as np 
import pandas as pd 
import datetime as dt
import sys
import seaborn as sns
import warnings 
from sklearn import preprocessing 
from datetime import datetime
import calendar
import locale
import statsmodels.api as sm

warnings.simplefilter('ignore')

Step2:データを読み込み、前処理


・データとしては『features data』,『sales data』,『stores data』の3つが与えられている。
・カテゴリー変数のカラムは数値化する。
isnullで欠損値が多いこと分かったので削除せずにそのままにしておく。
・3つのデータを結合する。


①features dataの各カラム:45の店舗番号、日付、平均天気、燃料費、MarkDown 1〜5(値引き)、消費者物価指数、失業率、特別休日の有無

features = pd.read_csv('/content/Features_data _set.csv')
features
features_dataの一部

②sales dataの各カラム:45の店舗番号、部門、日付、売上、特別休日の有無

sale = pd.read_csv("/content/sales_data_set.csv",encoding='utf-8')
sale
sales_dataの一部

③stores dataの各カラム:45の店舗番号、店舗種類、店舗サイズ

store = pd.read_csv('/content/stores_data_set.csv')
store
stores_dataの一部

Step3:データの可視化


結合したデータをヒートマップで見てみる

fig = plt.figure(figsize=(10,10))
sns.heatmap(
    train.corr(),cmap= sns.color_palette('coolwarm', 10),
    vmax=1,vmin=-1,annot=True
    )

ヒートマップでの分析


・markdown1とmarkdown4の正の相関がかなり高い(0.82)→markdown1とmarkdown4では値引きが似ている?
・markdown3とIsHolidayの正の相関がやや高い(0.43)→markdown3は休日にすることが多い?
・markdown1とsizeの正の相関がやや高い(0.35)→markdown1の値引き額は店舗の大きさによって違う?
・markdown2とIsHolidayの正の相関がやや高い(0.33)→markdown2は休日にすることが多い?
・markdown5とsizeの正の相関がやや高い(0.3)→markdown5の値引き額は店舗によって違う?
・markdown2とtemperatureの負の相関がやや高い(-0.32)→気温が上がるにつれmarkdown2の値引きが下がる = 裏を返せば気温が下がる時期に値引きしている?

各markdownとWeekly_salesの関係を可視化

fig = plt.figure(figsize=(10,10))
plt.xlabel("MarkDown1", fontsize=20)
plt.ylabel("Weekly_Sales", fontsize=20) 
fig = plt.scatter(train.MarkDown1.values,train.Weekly_Sales.values,marker="D",c=train.Size,cmap="Blues")
fig = plt.colorbar()
※ここではMarkdown1のみだが、同様に2 〜5も実施


3年分あるデータ(2010年〜2012年)を、1年ごとに売上(Weekly_Sales)の推移を可視化する。その際に各週ごとの平均を求めて見やすくする。

# グラフのタイトルを定める
plt.title("Weekly_Sales_of_2012")
# グラフのx軸とy軸の名前設定
plt.xlabel("time")
plt.ylabel("Weekly_Sales")

date_sales2 = date_sales.groupby(date_sales.index).aggregate(np.mean)

plt.plot(date_sales2)
plt.xlim(dt0,dt1)
plt.xticks(rotation=90)
plt.show()
※ここでは2012年のデータのみだが、同様に2011年、2010年も同様に実施
# グラフのタイトルを定める
plt.title("2010~2012")
# グラフのx軸とy軸の名前設定
plt.xlabel("time")
plt.ylabel("Weekly_Sales")

date_sales2010_2011_2012 = pd.DataFrame({"Weekly_Sales":
list(train["Weekly_Sales"])},index=list(train["Date"]))

dt6 = datetime.strptime("2010-02-05","%Y-%m-%d")
dt7 = datetime.strptime("2012-10-26","%Y-%m-%d")



date_sales2010_2011_2012 = date_sales2010_2011_2012.groupby(date_sales2010_2011_2012.index).aggregate(np.mean)

plt.plot(date_sales2010_2011_2012)
plt.xlim(dt6,dt7)
plt.xticks(rotation=90)
plt.show()


売上推移からの考察

【2012年】
・6月,8月,10月,7月,9月の順に売上が高い←MarkDownと相関があるのでは(単純にキャンペーンを多く実施したほうが売上が高い)?
・9月を除く各月の第1,2週目の売上が高い傾向(=9月だけ下がっている)←9月に何があった?返品?
【2011年】
・全体的に右肩上がり(11月末から売上が大幅に増加)←おそらくMarkDownの影響?
・9月にも少し売上が増加←MarkDown以外の影響か。だとしたら何?
【2010年】
・9月末~10月初旬にかけて売上がガクッと下がっている←2012年も下がっているので季節の影響?
・12月末,11月末~12月初旬,10月中旬の順に売上が大幅に増加←MarkDown以外の影響?だとしたら何?
【その他】
・そもそもWeekly_Salesに外れ値はないのか?
・トレンドと周期変動(季節変動)はなさそう


2010年〜2012年データを季節調整済み系列に変換して(季節調整を行い現系列をトレンド、季節変動、残差に分けて)見てみる

fig = sm.tsa.seasonal_decompose(date_sales2010_2011_2012, freq=4).plot()
plt.show()

トレンド、季節の影響を除去する

 #2010 ~2012年のトレンドを移動平均法で確認する


# 原系列のグラフ
plt.subplot(6, 1, 1)
plt.xlabel("time")
plt.ylabel("Weekly_Sales")
plt.plot(date_sales2010_2011_2012)
# 移動平均を求める
date_sales2010_2011_2012_moving_avg = date_sales2010_2011_2012.rolling(window=1).mean()
# 移動平均のグラフ
plt.subplot(6, 1, 3)
plt.xlabel("time")
plt.ylabel("Weekly_Sales")
plt.plot(date_sales2010_2011_2012_moving_avg)
# 原系列-移動平均グラフ
plt.subplot(6, 1, 5)
plt.xlabel("time")
plt.ylabel("Weekly_Sales")
mov_diff_date_sales2010_2011_2012 = date_sales2010_2011_2012 -  date_sales2010_2011_2012_moving_avg
plt.plot(mov_diff_date_sales2010_2011_2012)
plt.show()


Sizeごとに見てみる


・どういう基準で分類するのか
→四分位数を参考にSizeで4つに分ける
first_Size(最小値~第一四分位数):34875.000000~114533.000000
second_Size(第一四分位数~中央値):114533.000001~140167.000000
third_Size(中央値~第二四分位数):140167.000001~203742.000000
fourth_Size(第二四分位数~最大値):203742.000001~219622.000000

 #first_Size (最小値~第一四分位数):34875.000000~114533.000000

first_Size = train[(train["Size"]  <= 114533.000000)]
first_Size
 #first_Sizeのヒートマップ 

fig = plt.figure(figsize=(10,10))
sns.heatmap(
    first_Size.corr(),cmap= sns.color_palette('coolwarm', 10),
    vmax=1,vmin=-1,annot=True
    )
※ここではMarkdown1のみだが、同様に2 〜5も実施


IsHolidayの有無で分けて見てみる

train.groupby('IsHoliday').mean()

IsHolidayを含む週をヒートマップで可視化

 #IsHoliday_1のヒートマップ 

fig = plt.figure(figsize=(10,10))
sns.heatmap(
    IsHoliday_1.corr(),cmap= sns.color_palette('coolwarm', 10),
    vmax=1,vmin=-1,annot=True
    )
※ここではIsHolidayが1のみだが、同様に0でも実施


IsHolidayを含む週での分析

・MarkDown1とMarkDown5の正の相関が高い(0.69)
→IsHoidayを含む州ではこの2つの値引きのタイミングは似ている?
・MarkDown1とmarkDown3の負の相関が強い(-0.46)
→IsHolidayを含む週ではこの2つの値引きのタイミングは同じではないことが多い?

IsHolidayを含まない週の分析

・MarkDown1とMarkDown4の正の相関が高い(0.83)
→IsHolidayを含まない週ではこの2つの値引きのタイミングは似ている?

IsHolidayを含む週の売上は平均値、最大値共に高く、最小値(返品額?)は低いことが分かる。

IsHoliday_1.describe()
IsHolidayを含む週
IsHoliday_0.describe()
IsHolidayを含まない週


店舗種類(Store)ごとをヒートマップで可視化

 #store1  

Store_1  = train[train.Store==1]

fig = plt.figure(figsize=(10,10))
sns.heatmap(
    Store_1.corr(),cmap= sns.color_palette('coolwarm', 10),
    vmax=1,vmin=-1,annot=True
    )
※ここではStore1のみだが、同様にStore2〜Store45まで実施


店舗種類(Store)ごとの平均値、最大値、最小値で比較する

Store_average = train.groupby('Store').mean()
Store_average
※ここでは平均値のみだが、同様に最大値と最小値も実施

店舗種類(Store)ごとの平均値、最大値、最小値での分析


・全体を通じてIsHolidayとMarkDown2に正の相関、IsHolidayとMarkDown3に正の相関、Markdown2と気温に負の相関が強いことが分かる
・store1では燃料費とCPIの正の相関が強い(0.76)
・Store1~14、16~18、20~23、25、26、28~33、36~39はCPIと失業率の負の相関がかなり強い。Store24は正の相関が強い(0.86)
・Store15,19,27,34,35は相関ほぼなし
・Store23ではWeekly_Salesとdeptに正の相関が強い(0.6)
・Store24では燃料費と失業率にも正の相関が強い(それ以外では負の相関がある) →普通に考えると燃料費の高騰と失業率は正の相関が強くなるのでは?
・Weekly_Salesの平均が大きい(20000以上の)Store1,2,4,6,10,13,14,19,20,27,39は10以外Type-A(10のみType-B)、ほとんどのStoreのSizeが大きい(20000超えが多い)、 MarkDown1~5が大きい
・Weekly_Salesの平均が小さい(10000未満の)Store3,7,9,15,16,29,30,33,36,38,44はほとんどのStoreのSizeが小さい、33と36以外はType-BかType-C(33と36はType-A)、MarkDown1~5が小さい
・各店舗のWeekly_Salesが最大値の時は、2012年10月26日(IsHolidayを含む週)で、Deptがすべて98か99。
・Weekly_Salesの最大値が400000以上のStore10,14,20,27,35はすべてType-AかType-B、MarkDown1~5が大きい。10と35はWeekly_Salesの最大値が1番と2番で600000超え(どちらもType-Bで、Sizeはそこまで大きくない)。Store14,20,27はSizeが200000超え
・Weekly_Salesの最大値が10000を下回っているStore5,30,33,36,37,44はSizeが小さく(40000未満)、MarkDown1~5も小さい
・Weekly_Salesが最小値の日は全ての店舗が2010年2月5日、Weekly_salesがマイナス、Deptが1
・Weekly_Salesの最小値が小さい(-1000未満の)Storeは2,3,16,28,35,45。その中でも1番と2番はMarkDown2と3が小さい(1,4,5は大きい)
・Weekly_Salesの最小値が大きい(-100より大きい)Storeは13,21,25,26,30,33,37で、MarkDownが全体的に小さい


Weekly_Salesの自己相関係数・偏自己相関係数の可視化

特に言えることはなさそう

 #自己相関・偏自己相関の可視化 
fig = plt.figure(figsize=(12,8)) #自己相関係数のグラフを出力する 
ax1 = fig.add_subplot(211)

fig = sm.graphics.tsa.plot_acf(x.Weekly_Sales,lags=10,ax=ax1) #偏自己相関係数のグラフを出力する 
ax2 = fig.add_subplot(212)
fig = sm.graphics.tsa.plot_pacf(x.Weekly_Sales,lags=10,ax=ax2)
plt.show()


月別で見てみる

 #月ごとで分類 

month_date = train.copy()
month_date["Date"] = pd.to_datetime(month_date["Date"])
month_date.set_index("Date",inplace=True)
month_date = month_date.resample("M").mean()
month_date

月別の分析

【2012年】
・2月の売上が高いのはIsHolidayを含む月だからではないか
【2011年】
・11月から売上が上がっているのは単純にIsHolidayを含む月だからではないか。MarkDownを実施していない2010年の11月と12月の売上金額と大差ないため、MarkDownの効果ではない?
・9月にも売上が上がっているが、IsHolidayを含む月だからではないか?
【2010年】
・11月と12月のWeekly_Salesが高いのは、どちらもIsHolidayを含む月だからではないか?

Step4:学習用データとテスト用データに分ける

from sklearn.model_selection import train_test_split

X_train,X_test,y_train,y_test = train_test_split(explanatory,target,test_size=0.20,random_state=2)


Step5:LightGBMモデルの学習

決定木アルゴリズムに基づく勾配ブースティング(GBDT)の一種であるLightGBMを用いて予測モデルを作成し、評価する。
LightGBMの利点としては『予測精度が高い』,『学習時間が比較的短い』,『どの特徴量が重要か算出できる』,『欠損値をそのまま扱える』,『不要な特徴量を入れても精度が落ちにくい』などが挙げられる。

①scikit-learn APIバージョン

 #予測値の確認 
 #真値と予測値の表示表示  #df_pred  = pd.DataFrame({'CRIM':y_test,'CRIM_pred':y_pred}) #display (df_pred)
 #散布図を描写 (真値値と予測値)
plt.plot(y_test,y_test,color = 'red',label = 'x=y')
plt.scatter(y_test,y_pred)
plt.xlabel('y')
plt.ylabel('y_test')
plt.title('y vs y_pred') 
 #モデル評価  #rmse  : 標準誤差の平方根
mse = mean_squared_error(y_test,y_pred)   #MSE (平均二乗誤差)の算出
rmse = np.sqrt(mse)    #RSME  = √√MSEの算出
print('RMSE:',rmse)

# r2 : 決定係数
r2 = r2_score(y_test,y_pred)
print('R2:',r2) 

scikit-learn APIバージョンの考察

・R2(決定係数)からモデルの当てはまりは良い
・RMSE (平均二乗誤差の平方根)から、モデルの予測精度が悪い→外れ値の値が大きいから
・特徴量の重要度のプロットはできない?


②Python APIバージョン

# 特徴量の重要度のプロット
feature_importances = model1.feature_importance()
fi_index = np.argsort(feature_importances)
fi_index
plt.barh(explanatory.columns[fi_index], feature_importances[fi_index])
 #予測値の確認 
 #真値と予測値の表示  #df_pred1  = pd.DataFrame({'CRIM':test_y,'CRIM_pred':y_pred1}) #display (df_pred1)
 #散布図を描画 (真値 vs 予測値)
plt.plot(test_y,test_y,color = 'red',label = 'x=y')
plt.scatter(test_y,y_pred1)
plt.xlabel('y')
plt.ylabel('test_y')
plt.title('y vs y_pred1')
 #モデルの評価 
 #rmse  : 平均二乗誤差の平方根平方根
mse = mean_squared_error(test_y,y_pred1)
rmse = np.sqrt(mse)
print('RMSE :',rmse)
 #r2  : 決定係数
r2 = r2_score(test_y,y_pred1)
print('R2 :',r2)

Python APIバージョンの考察

・R2(決定係数)からモデルの当てはまりは良い
・RMSE (平均二乗誤差の平方根)はsikit-learnバージョンよりかは改善したが、依然として予測精度が悪い→外れ値の値が大きい
・特徴量の重要度をプロットしたところ、上から部門、天気、店舗サイズの順に高い。また、MarkDown同士で見ると3,2,5,4,1の順に高い


IsHolidayの有無でモデル学習させる


IsHolidayを含む週での学習

# 特徴量の重要度のプロット
feature_importances = model_IsHoliday1.feature_importance()
fi_index = np.argsort(feature_importances)
fi_index
plt.barh(explain_IsHoliday1.columns[fi_index], feature_importances[fi_index])
 #モデルの評価 
 #rmse  : 平均二乗誤差の平方根平方根
mse = mean_squared_error(test_y,y_pred_IsHoliday1)
rmse = np.sqrt(mse)
print('RMSE :',rmse)
 #r2  : 決定係数
r2 = r2_score(test_y,y_pred_IsHoliday1)
print('R2 :',r2)

IsHoliday=1での学習の考察

・R2(決定係数)からモデルの当てはまりはまずまずと言える
・RMSE (平均二乗誤差の平方根)から予測精度が悪い→外れ値が大きいため
・説明変数の需要度において、上から部門、店舗サイズ、消費者物価指数の順に高い。MarkDownについては3,2,5,1,4の順に高い


IsHolidayを含まない週での学習

# 特徴量の重要度のプロット
feature_importances = model_IsHoliday0.feature_importance()
fi_index = np.argsort(feature_importances)
fi_index
plt.barh(explain_IsHoliday0.columns[fi_index], feature_importances[fi_index])
 #モデルの評価 
 #rmse  : 平均二乗誤差の平方根平方根
mse = mean_squared_error(test_y,y_pred_IsHoliday0)
rmse = np.sqrt(mse)
print('RMSE :',rmse)
 #r2  : 決定係数
r2_IsHoliday0 = r2_score(test_y,y_pred_IsHoliday0)
print('R2 :',r2_IsHoliday0)

IsHolidayを含まない週での分析

・R2(決定係数)からモデルの当てはまりはまずまずと言える
・RMSE (平均二乗誤差の平方根)から予測精度が悪い→外れ値が大きいため
・説明変数の需要度において、上から部門、店舗サイズ、消費者物価指数の順に高い。MarkDownについては3,4,2,5,1の順に高い

振り返り

今回、データ分析の基本的な流れを掴めたのは大きな収穫です。今後の学習に向けて非常に良い経験ができたと思っています。
今回は欠損値の多く、欠損値を含む行の削除を行うとデータ数が減ってしまうと判断しました。そのため、モデル選定では欠損値をそのままでも使えるLightGBMを採用しました。次回は欠損値を平均値へ置き換えや外れ値の処理であったり、パラメーターチューニングなどを行いたいです。また、様々なモデルでの学習も行いたいです。

この記事が気に入ったらサポートをしてみませんか?