あらすじ
統計検定を無事に終えた僕。
多少数式にも慣れてきて勉強のモチベーションも上がり始めたところで、満を持してkaggleのDSBコンペ(https://www.kaggle.com/c/data-science-bowl-2019)に参加することに。
メダル圏内を目指してkernelを読み漁るも内容理解が捗らない and 自分でコードを書くことに慣れていないためアワアワする。
僕「どうしよう…色々アイディアは出たのに表現できない><」
実際にコードを書く作業を怠ってきたため、オリジナルのコードを書けないのだった。
僕「もっと簡単なテーブルデータで練習しなきゃ…」
そこで見つけたsignate練習問題。
程よいサイズ感と理解しやすい内容で練習にはうってつけじゃないか!
というわけでsignateの練習問題【お弁当の重要予測】に取り組んでみました。
※最初は自力で取り組みましたが結果が振るわず、途中からチュートリアルを参考に勧めました。
データ概要とEDA
https://signate.jp/competitions/24
とある会社のカフェフロアで販売しているお弁当の販売数を予測します。 最初にtrainデータの確認。
train.head()
datetime | y | week | soldout | name | kcal | remarks | event | payday | weather | precipitation | temperature | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 2013-11-18 | 90 | 月 | 0 | 厚切りイカフライ | NaN | NaN | NaN | NaN | 快晴 | -- | 19.8 |
1 | 2013-11-19 | 101 | 火 | 1 | 手作りヒレカツ | NaN | NaN | NaN | NaN | 快晴 | -- | 17.0 |
2 | 2013-11-20 | 118 | 水 | 0 | 白身魚唐揚げ野菜あん | NaN | NaN | NaN | NaN | 快晴 | -- | 15.5 |
3 | 2013-11-21 | 120 | 木 | 1 | 若鶏ピリ辛焼 | NaN | NaN | NaN | NaN | 快晴 | -- | 15.2 |
4 | 2013-11-22 | 130 | 金 | 1 | ビッグメンチカツ | NaN | NaN | NaN | NaN | 快晴 | -- | 16.1 |
train.describe(include="O") #O(オー)
datetime | week | name | remarks | event | weather | precipitation | |
---|---|---|---|---|---|---|---|
count | 207 | 207 | 207 | 21 | 14 | 207 | 207 |
unique | 207 | 5 | 156 | 6 | 2 | 7 | 8 |
top | 2014-7-18 | 水 | メンチカツ | お楽しみメニュー | ママの会 | 快晴 | -- |
freq | 1 | 43 | 6 | 12 | 9 | 53 | 169 |
train.index=pd.to_datetime(train.datetime) plt.figure(figsize=(15,4)) plt.xlabel("datetime") plt.ylabel("y") plt.plot(train.y)
かなり販売数が上下しています。何が起因しているのでしょうか? 続いて各カラムについて、販売数との相関を確認します。 その前に欠損値を埋めていきます。
train.info()の内容追加
train.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 207 entries, 0 to 206
Data columns (total 12 columns):
datetime 207 non-null object
y 207 non-null int64
week 207 non-null object
soldout 207 non-null int64
name 207 non-null object
kcal 166 non-null float64
remarks 21 non-null object
event 14 non-null object
payday 10 non-null float64
weather 207 non-null object
precipitation 207 non-null object
temperature 207 non-null float64
dtypes: float64(3), int64(2), object(7)
memory usage: 19.5+ KB
train.payday = train.payday.fillna(0) train.precipitation = train.precipitation.apply(lambda x:-1 if x == "--" else float(x)) train.event = train.event.fillna("none") train.remarks = train.remarks.fillna("none") train["year"] = train.datetime.apply(lambda x:int(x.split("-")[0])) train["month"] = train.datetime.apply(lambda x:int(x.split("-")[1]))
先ほどのグラフより、時間が経つとともに売り上げが下がっている傾向が見られたので、カラムに"year"と"month"を追加しました。
fig, ax = plt.subplots(2,3,figsize=(9,6)) train.plot.scatter(x="soldout", y="y", ax=ax[0][0]) train.plot.scatter(x="kcal", y="y", ax=ax[0][1]) train.plot.scatter(x="precipitation", y="y", ax=ax[0][2]) train.plot.scatter(x="payday", y="y", ax=ax[1][0]) train.plot.scatter(x="temperature", y="y", ax=ax[1][1]) train.plot.scatter(x="month", y="y", ax=ax[1][2]) plt.tight_layout() train.plot.scatter(x="year", y="y")
気温と販売数は相関がありそう。 あったかいと外でお昼を食べるのかな? そうなると天気も関係が出てきそうですね。
fig, ax = plt.subplots(2,2,figsize=(12,7)) sns.boxplot(x="week",y="y",data=train,ax=ax[0][0]) sns.boxplot(x="weather",y="y",data=train,ax=ax[0][1]) sns.boxplot(x="remarks",y="y",data=train,ax=ax[1][0]) ax[1][0].set_xticklabels(ax[1][0].get_xticklabels(),rotation=30) sns.boxplot(x="event",y="y",data=train,ax=ax[1][1]) plt.tight_layout()
天気が悪いと販売数が少ない。 remarksは売り上げに大きく影響がありそうです。
メニューは多くの種類がありますが、複数回登場するメニューも一部存在します。 おそらく人気メニューなので売り上げが高いことが予想できます。
second = train.name.value_counts() second = second[second>1] print(second) train["second"] = train.name.apply(lambda x:1 if x in second.index else 0)
メンチカツ 6
タンドリーチキン 6
手作りロースカツ 5
マーボ豆腐 4
鶏の唐揚げおろしソース 4
回鍋肉 4
肉じゃが 4
チキンカレー 3
キーマカレー 3
チンジャオロース 3
鶏の味噌漬け焼き 3
手作りひれかつ 3
ポークカレー 3
酢豚 3
青椒肉絲 2
ロース甘味噌焼き 2
手作りチキンカツ 2
手作りヒレカツ 2
鶏のカッシュナッツ炒め 2
マーボ茄子 2
ハンバーグカレーソース 2
鶏チリソース 2
カレイ唐揚げ野菜あんかけ 2
ボローニャ風カツ 2
ハンバーグデミソース 2
Name: name, dtype: int64
sns.barplot(x="second", y="y", data=train)
予想通り、複数回登場するメニューは比較的販売数が多そうです。 さらに内訳を見てみましょう。
plt.figure(figsize=(15,5)) plt.plot(train[train["second"] == 1].groupby("name")["y"].mean()) plt.xticks(rotation=70)
特にお弁当がカレーの時に販売数が多いようです。 他にもメニューと販売数の相関(揚げ物が人気etc)がありそうですが、つかみ切ることはできませんでした。
予測モデル作成
一通りtrainデータを眺めたので、予測モデルを作っていきます。
train.index = train["datetime"] #LightGBMは日本語対応していないのでラベルに変換 l = ["weather", "week"] for c in l: le = LabelEncoder() le.fit(train[c]) train[c] = le.transform(train_test[c]) #数値データを適当に4分割 train.kcal = pd.qcut(train.kcal, 4) train.temperature = pd.qcut(train.temperature, 4) train.remarks = np.where(train.remarks.isnull(), 0, 1) train.event = np.where(train.event.isnull(), 0, 1) train.payday = np.where(train.payday.isnull(), 0, 1) second = train.name.value_counts() second = second[second>1] train["second"] = train.name.apply(lambda x: 1 if x in second.index else 0) train["year"] = train.datetime.apply(lambda x:1 if int(x.split("-")[0]) == 2013 else 0) train["month"] = train.datetime.apply(lambda x:int(x.split("-")[1])) train["curry"]= train.name.apply(lambda x:1 if x.find("カレー")>=0 else 0) #使わなそうなカラムを削除 del train["precipitation"] del train["name"] #ダミー変数に変換 train = pd.get_dummies(train, columns=["event", "payday", "remarks", "soldout", "temperature", "weather", "week", "second", "year", "month", "curry"]) X_train = train.drop(["y", "datetime"], axis=1) y_train = train["y"] #データを分割 train_x, test_x, train_y, test_y = train_test_split(X_train, y_train, test_size=0.33, random_state=0) #線形回帰 lr = LinearRegression() lr.fit(train_x, train_y) p = pd.DataFrame({"actual":test_y, "pred": lr.predict(test_x)}) p.plot(figsize=(15,4)) lr.score(test_x, test_y) #0.7314778590822022
7割の一致率でした。
リッジ回帰でも試してみます。
ll = [0.0001, 0.001, 0.01, 0.1, 1, 10] for i in ll: ridge = Ridge(alpha=i).fit(train_x, train_y) print("train_score", ridge.score(train_x, train_y)) print("test_score", ridge.score(test_x, test_y)) print("===========================================")
train_score 0.8096849640192347
test_score 0.719443975048337
===========================================
train_score 0.8096849473156098
test_score 0.7194314722710842
===========================================
train_score 0.8096833077204609
test_score 0.7193071060465293
===========================================
train_score 0.8095453711358758
test_score 0.718129702247557
===========================================
train_score 0.8047733756449506
test_score 0.7109998915048675
===========================================
train_score 0.7504625629928654
test_score 0.6656301365846875
===========================================
ridge = Ridge(alpha=0.001) ridge.fit(train_x, train_y) p = pd.DataFrame({"actual":test_y, "pred": ridge.predict(test_x)}) p.plot(figsize=(15,4))
線形回帰よりも若干スコアが下がりました。なぜだろう?
model = lgb.LGBMRegressor() model.fit(train_x, train_y) model.score(test_x, test_y)
0.5393031683517218
ちなみにkaggleで主流となっているLightGBMを用いたらスコアが大きく下がりました。
分かったこと、身に付いたこと
・エラーを何度も乗り越えて、自分でコードを書いていく楽しさ(辛さ) →エラーが消えた時の喜びは異常 ・データの基本的な処理方法 →以前まではいちいち調べながら書いていたため時間効率が悪かった ・特徴量の見出し方、データの読み方
分からないこと、今後勉強したいこと
・スコアが伸びなかった理由 →ほかのモデルを試す、パラメータ調整を試す 予測モデルは中身がブラックボックスなので使いどころが分からないのが最大の問題な気がします。 完全に理解するのは時間がかかると思うので、使いながらゆっくり身に付けていこうと思います。 線形モデルは特徴量の数値処理がスコアに大きく影響を与えることがあるようなので、そこも勉強していきたいです。
・アンサンブル チュートリアルでは線形回帰と決定木をアンサンブルして高スコアを出していました。 アンサンブルはまだ試したことがないので今後触れていきたいです。