東京ドーム（巨人戦）の観客数予測をやってみた

# 目次
[1.はじめに](#1-はじめに)
[2.流れ](#2-流れ)
[3.スクレイピングを用いてデータ取得](#3-スクレイピングを用いてデータ取得)
[4.データの前処理](#4-データの前処理)
[5.機械学習モデルの作成](#5-機械学習モデルの作成)
[6.修正してモデルの向上を目指す](#6-修正してモデルの向上を目指す)
[7.おわりに](#7-おわりに)

# 1. はじめに

毎日東京ドームを満員にする良い案を考える為、手始めの調査として現状の情報を基に観客数の予測をしてみました。
天気、対戦相手、先発投手、曜日を影響があると仮定して説明変数としたいと思います。
本当はその日の巨人主催のイベント、新グッズ等も入れて予測したい所ですが、初心者で慣れていないということもあり、一先ず無しで先に進みます。
使用環境：Macbookair,googlecolabratory

# 2. 流れ
1,スクレイピングでデータを取得
2,データの前処理

# 3. スクレイピングを用いてデータ取得

まずデータの取得は下記2つのサイトより収集しました。
①[プロ野球Freak](https://baseball-freak.com/audience/16/giants.html)より観客数などの情報
②[気象庁](https://www.data.jma.go.jp/risk/obsdl/index.php)より東京の天気概況

①の観客数データに関しては、まずスプレッドシートにコピーしてから読み取りました。
②の天気概況に関しては、csvファイルを取得出来たので、そちらを使用しました。

①↓
```
import pandas as pd
import io

from google.colab import auth
auth.authenticate_user()

import gspread
from google.auth import default
creds, _ = default()

gc = gspread.authorize(creds)

url = '[https://docs.google.com/spreadsheets/d/1cZ3sOPblhof7KRKoon40-rgqznGjSVmd9kJksC3X914/edit#gid=0]'

worksheet = gc.open_by_url(url).get_worksheet(0)
df2 = pd.DataFrame(worksheet.get_all_values())
```

②↓
```
from google.colab import files

uploaded = files.upload()

import pandas as pd
import io

df = pd.read_csv(io.BytesIO(uploaded['天気data2016~.csv']), sep=",", encoding="shift-jis")
df.reset_index(inplace = True)
```

# 4. データの前処理

読み込んだデータを学習出来るように、クレンジングしていきます。
まずは①の野球データ↓
列の名前を入れて、日付データを整えています。
元のデータが【年/月/日】ではなく【月/日】だった為、少し苦慮しました。for文を使ってもう少し上手く書けそうです。
```
from google.colab import auth
auth.authenticate_user()

import gspread
from google.auth import default
creds, _ = default()

gc = gspread.authorize(creds)

url = '[https://docs.google.com/spreadsheets/d/1cZ3sOPblhof7KRKoon40-rgqznGjSVmd9kJksC3X914/edit#gid=0]'

worksheet = gc.open_by_url(url).get_worksheet(0)
df2 = pd.DataFrame(worksheet.get_all_values())
df2.columns = ["仮日付","観客数","対戦相手", "投手名"]
df2["日付"] = str(2016) + "年" + df2["仮日付"]
df2["日付"] = df2["日付"].apply(lambda x:x[:-3])
df2["日付"] = pd.to_datetime(df2['日付'], format='%Y年%m月%d日')
df2=df2.drop(["仮日付"], axis=1)

worksheet = gc.open_by_url(url).get_worksheet(1)
df3 = pd.DataFrame(worksheet.get_all_values())
df3.columns = ["仮日付","観客数","対戦相手", "投手名"]
df3["日付"] = str(2017) + "年" + df3["仮日付"]
df3["日付"] = df3["日付"].apply(lambda x:x[:-3])
df3["日付"] = pd.to_datetime(df3['日付'], format='%Y年%m月%d日')
df3=df3.drop(["仮日付"], axis=1)

worksheet = gc.open_by_url(url).get_worksheet(2)
df4 = pd.DataFrame(worksheet.get_all_values())
df4.columns = ["仮日付","観客数","対戦相手", "投手名"]
df4["日付"] = str(2018) + "年" + df4["仮日付"]
df4["日付"] = df4["日付"].apply(lambda x:x[:-3])
df4["日付"] = pd.to_datetime(df4['日付'], format='%Y年%m月%d日')
df4=df4.drop(["仮日付"], axis=1)

worksheet = gc.open_by_url(url).get_worksheet(3)
df5 = pd.DataFrame(worksheet.get_all_values())
df5.columns = ["仮日付","観客数","対戦相手", "投手名"]
df5["日付"] = str(2019) + "年" + df5["仮日付"]
df5["日付"] = df5["日付"].apply(lambda x:x[:-3])
df5["日付"] = pd.to_datetime(df5['日付'], format='%Y年%m月%d日')
df5=df5.drop(["仮日付"], axis=1)

worksheet = gc.open_by_url(url).get_worksheet(4)
df6 = pd.DataFrame(worksheet.get_all_values())
df6.columns = ["仮日付","観客数","対戦相手", "投手名"]
df6["日付"] = str(2022) + "年" + df6["仮日付"]
df6["日付"] = df6["日付"].apply(lambda x:x[:-3])
df6["日付"] = pd.to_datetime(df6['日付'], format='%Y年%m月%d日')
df6=df6.drop(["仮日付"], axis=1)

df7 = pd.concat([df2,df3,df4,df5,df6], axis=0)

df7["観客数"] = df7["観客数"].apply(lambda x:x.replace(",","").replace("人", "")).astype("int64")

df7 = df7.reset_index(drop=True)

display(df7)
```
次に②の天気データも必要ない部分を削り、日付と天気のみにします。
```
import pandas as pd
import io

df1 = pd.read_csv(io.BytesIO(uploaded['天気data2016~.csv']), sep=",", encoding="shift-jis")
df1.reset_index(inplace = True)
df1 = df1.iloc[4:, [0,1]]
df1.columns = ["日付", "天気"]

import datetime

df1["日付"] = pd.to_datetime(df1["日付"])

display(df1)
```
次は野球データと天気データを併せます。
そして「ヤクルト」、「菅野」、「雨」などのままでは学習できない為、全て数値化していきます。
```
df8 = pd.merge(df7, df1, on='日付', how='left')

dummy = pd.get_dummies(df8[["対戦相手","投手名","天気"]])
dummy = pd.concat([df8, dummy], axis=1).drop(["対戦相手","投手名","天気"], axis=1)

dummy["日付"] = dummy["日付"].view("int64")
display(dummy)
```
データは下記のような形になり、観客数をy（目的変数）、それ以外をX（説明変数）としたいと思います。
![スクリーンショット 2022-10-15 15.23.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2898191/661fc736-dc7f-42e4-505b-4671d1e86a84.png)


念のため欠損値がないかの確認をします。
```
dummy.isnull().sum()
```

# 5. 機械学習モデルの作成
データが整ったので、いよいよ機械に学習させていきます。
アイデミーで習った線形重回帰、ラッソ回帰、リッジ回帰を試してみます。
>リッジ回帰・・他と余りにもかけ離れたデータの重みを0にする事で、学習用データから削除する
ラッソ回帰・・他と余りにもかけ離れたデータの大きさに応じて0に近づけて、学習結果を滑らかにする
特徴量が多く、そのうち重要なものは僅かしかないことが予測されるのであれば、ラッソ回帰が適しています。


```
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.linear_model import Lasso
from sklearn.linear_model import Ridge

X = dummy.drop(["観客数"], axis=1)
y = dummy["観客数"]

train_X, test_X, train_y, test_y = train_test_split(X, y, test_size=0.1, random_state=123)

max_score = 0
best_model = ""
scores = []

# 線形回帰
model = LinearRegression()
model.fit(train_X, train_y)
score = model.score(test_X, test_y)
scores.append(score)
if max_score < score:
    max_score = score
    best_model = "線形回帰"
# ラッソ回帰
model = Lasso()
model.fit(train_X, train_y)
score = model.score(test_X, test_y)
scores.append(score)
if max_score < score:
    max_score = score
    best_model = "ラッソ回帰"
# リッジ回帰
model = Ridge()
model.fit(train_X, train_y)
score = model.score(test_X, test_y)
scores.append(score)
if max_score < score:
    max_score = score
    best_model = "リッジ回帰"

print("モデル:{}".format(best_model))
print("決定係数:{}".format(max_score))

import matplotlib.pyplot as plt
label = ["LinearRegression", "Ridge", "Lasso"]
left = [1,2,3]
height = scores
plt.bar(left, height, tick_label=label, align="center")
```
結果はこのような形になりました。
>モデル:線形回帰
決定係数:0.4523374827658594
![観客数予測係数①.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2898191/963de62d-3023-3db9-acf3-ed4d8f9eb820.png)


あまり良い結果は得られませんでした。
1年で60試合程度なので、5年分でも単純にデータ数が300件程度と少なかった事は要因の1つと考えられます。
他にも、、
・天気の種類が細かすぎる事
・曜日の要素が上手く入ってない事
・イベントの有無、新グッズ発売有無、チームの順位など影響がありそうな要素を入れていない事
・学習モデルが合っていない
などが要因ではないかと考えます。

# 6. 修正してモデルの向上を目指す
まず開幕直後や、シーズン終了間際など人が集まる時期の要素を入れる為に「気温」を気象庁データに付け加えます。
併せて、日付けは平日を0、休日を1という形に変更。
天気概況も「雨」が含まれていれば0、その他を1としました。
```
import pandas as pd
import io

df1 = pd.read_csv(io.BytesIO(uploaded['2016~天気気温data.csv']), sep=",", encoding="shift-jis")
df1.reset_index(inplace = True)
df1 = df1.iloc[3:, [0,1,4]]
df1.columns = ["日付","天気","気温"]

import datetime

df1["日付"] = pd.to_datetime(df1["日付"])

! pip install jpholiday
import jpholiday
import re 
import datetime

temp = df1.copy()

#日本の祝日かもしくは土日かどうかを判定する関数を作成
def isBizDay(date):
    Date = datetime.date(int(date[0]), int(date[1]), int(date[2]))
    if Date.weekday() >= 5 or jpholiday.is_holiday(Date):
        return 1
    else:
        return 0

holiday = []
for i in range(len(temp)) :

 test = re.split("-|T",str(temp["日付"].values[i]))[:3]
 result = isBizDay(test)
 holiday.append(int(result))
df1["日付"] = holiday

for i in range(len(df1)) :
  if "雨" in df1.iloc[i,1] :
    df1.iloc[i,1] = 0
  else :
    df1.iloc[i,1] = 1

display(df1)
```
ランダムフォレストを追加して再度学習させます。
>ランダムフォレスト・・決定木による複数の弱学習器を統合させて汎化能力を向上させ,モデルの多数決で分類を決めるアンサンブル学習アルゴリズム
```
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression , Lasso, Ridge
from sklearn.ensemble import RandomForestRegressor
import numpy as np

X = dummy.drop(["観客数"], axis=1)
y = dummy["観客数"]

train_X, test_X, train_y, test_y = train_test_split(X, y, test_size=0.1, random_state=123)

max_score = 0
best_model = ""
scores = []

# 線形回帰
model = LinearRegression()
model.fit(train_X, train_y)
score = model.score(test_X, test_y)
scores.append(score)
print(score)
if max_score < score:
    max_score = score
    best_model = "線形回帰"
# ラッソ回帰
model = Lasso()
model.fit(train_X, train_y)
score = model.score(test_X, test_y)
scores.append(score)
print(score)
if max_score < score:
    max_score = score
    best_model = "ラッソ回帰"
# リッジ回帰
model = Ridge()
model.fit(train_X, train_y)
score = model.score(test_X, test_y)
scores.append(score)
print(score)
if max_score < score:
    max_score = score
    best_model = "リッジ回帰"

# ランダムフォレスト
model = RandomForestRegressor()
model.fit(train_X, train_y)
score = model.score(test_X, test_y)
scores.append(score)
if max_score < score:
    max_score = score
    best_model = "ランダムフォレスト"

print("モデル:{}".format(best_model))
print("決定係数:{}".format(max_score))

import matplotlib.pyplot as plt
np.set_printoptions(precision=3, suppress=False)
label = ["LinearRegression", "Ridge", "Lasso","RandomForest"]
left = [1,2,3,4]
scores = np.array(scores)
scores = np.round(np.array(scores), decimals = 3)
height = scores
plt.bar(left, height, tick_label=label, align="center")
print(scores)
```
>モデル:ランダムフォレスト
決定係数:0.5232233949781905
[0.411 0.411 0.411 0.523]
![図②.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2898191/9c35ff18-e8c2-e558-ebf1-633467413a9f.png)

少し改善されました。
やはりランダムフォレストになりました（その他はあまり変わっていません）。
feature importancesの棒グラフも出してみました。
```
!pip install japanize-matplotlib
import japanize_matplotlib
fi = model.feature_importances_
fn = list(X.columns)
plt.barh(fn, fi)
```
![特徴量.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2898191/df983a22-d9dc-a31f-ec67-58bdf3e0d4df.png)
気温だけがかなり効いている事が分かりました。
対戦相手は試合数が多いセリーグの数球団が少し影響してるようですが、影響力は小さいですね。
やはりもう少し影響力のある特徴量を持ってくる事、そしてそもそもの試合数データを増やす必要がありそうです。

# 7. おわりに
アドバイス頂きながらでしたが、データ取得〜データ前処理〜モデル作成〜予測とアウトプットする事が出来て、良い勉強になりました。
まだまだ難しく分からない事ばかりですが、初心者の自分でもなんとなく形に出来て、結果が目に見えて分かるのは非常に面白さを感じました。
卒業後も色々調べて試してを繰り返して、学習しつつ楽しみたいと思います。
有難うございました。