LoginSignup

This article is a Private article. Only a writer and users who know the URL can access it.
Please change open range to public in publish setting if you want to share this article with other users.

More than 1 year has passed since last update.

Python初学習が京都サンガFCの勝敗予想モデルを作ってみた

Last updated at Posted at 2021-08-23

はじめに

こんにちは。Python で機械学習を習得中のやままさです。
現職は電気メーカーでマーケティング企画を行っておりますが、データ分析の必要性を感じAidemyでデータ分析コースを受講しました。今回はその成果物として寄稿いたします。

目次

1.目的
2.私の環境
3.スクレイピングを用いて自動でデータを取得
4.データの前処理
5.取得した情報を元に機械学習モデルを作成
6.今後の課題

1 目的

私の愛する京都サンガFCの勝敗予想を行うモデルを構築することです。

2 私の環境

Python3
MacBook Air
Chrome
Google Colaboratory

3 スクレイピングを用いて自動でデータを取得

スクレイピングとは、Webサイトから必要な情報(テキスト、画像、リンクなど)を自動で抽出する作業を言います。今回はFootball LAB(https://www.football-lab.jp/) というサイトから京都サンガFCの試合データ(https://www.football-lab.jp/kyot/match/) を抽出いたしました。

サイトのデータ使用について下記の利用規約がございますので注意して使用いたします。
スクリーンショット 2021-11-28 18.57.40.png

!pip install beautifulsoup4
# resuests モジュールをインポート
import requests
from bs4 import BeautifulSoup

# Webページを取得
load_url = "https://www.football-lab.jp/kyot/match/"
html = requests.get(load_url)
soup = BeautifulSoup(html.content, "html.parser")

# HTML全体を表示する
print(soup)

# 認証する
auth.authenticate_user()
gc = gspread.authorize(GoogleCredentials.get_application_default())

for element in soup.find_all("td"):    # すべてのtdタグを検索して表示
    print(element.text)

ご参考に1試合分のデータを表示します。
スクリーンショット 2021-11-28 11.05.12.png

import pandas as pd

#シーズンごとにデータを取得(2012年〜2021年)
url1 = 'https://www.football-lab.jp/kyot/match/'
dfs1 = pd.read_html(url1)
url2 = 'https://www.football-lab.jp/kyot/match/?year=2020'
dfs2 = pd.read_html(url2)
url3 = 'https://www.football-lab.jp/kyot/match/?year=2019'
dfs3 = pd.read_html(url3)
url4 = 'https://www.football-lab.jp/kyot/match/?year=2018'
dfs4 = pd.read_html(url4)
url5 = 'https://www.football-lab.jp/kyot/match/?year=2017'
dfs5 = pd.read_html(url5)
url6 = 'https://www.football-lab.jp/kyot/match/?year=2016'
dfs6 = pd.read_html(url6)
url7 = 'https://www.football-lab.jp/kyot/match/?year=2015'
dfs7 = pd.read_html(url7)
url8 = 'https://www.football-lab.jp/kyot/match/?year=2014'
dfs8 = pd.read_html(url8)
url9 = 'https://www.football-lab.jp/kyot/match/?year=2013'
dfs9 = pd.read_html(url9)
url10 = 'https://www.football-lab.jp/kyot/match/?year=2012'
dfs10 = pd.read_html(url10)

4 データの前処理

#各シーズンにシーズン名をタグ付け
res1 = pd.DataFrame([['S2021']]*len(dfs1[0])).join(dfs1, lsuffix='0')
res2 = pd.DataFrame([['S2020']]*len(dfs2[0])).join(dfs2, lsuffix='0')
res3 = pd.DataFrame([['S2019']]*len(dfs3[0])).join(dfs3, lsuffix='0')
res4 = pd.DataFrame([['S2018']]*len(dfs4[0])).join(dfs4, lsuffix='0')
res5 = pd.DataFrame([['S2017']]*len(dfs5[0])).join(dfs5, lsuffix='0')
res6 = pd.DataFrame([['S2016']]*len(dfs6[0])).join(dfs6, lsuffix='0')
res7 = pd.DataFrame([['S2015']]*len(dfs7[0])).join(dfs7, lsuffix='0')
res8 = pd.DataFrame([['S2014']]*len(dfs8[0])).join(dfs8, lsuffix='0')
res9 = pd.DataFrame([['S2013']]*len(dfs9[0])).join(dfs9, lsuffix='0')
res10 = pd.DataFrame([['S2012']]*len(dfs10[0])).join(dfs10, lsuffix='0')
#各シーズンのデータを結合
rest = pd.concat([res1, res2, res3, res4, res5, res6, res7, res8, res9, res10]) 
from IPython.display import display
display(rest)

結合したデータは下記の通りです。
スクリーンショット 2021-11-28 11.09.05.png

#必要の無い列を削除
rest2 = rest.drop(['節', "Unnamed: 2", "得点者", "指揮官"], axis=1)

#「スコア」カラムの得点(tokuten)と失点(shitten)を差し引きし、「勝ち=w」「引き分け=d」「負け=l」をタグ付け
rest2["tokuten"] = rest2["スコア"].apply(lambda x: str(x)[0:1])
rest2["shitten"] = rest2["スコア"].apply(lambda x: str(x)[2:])
rest2.loc[rest2['tokuten'] > rest2["shitten"], 'wdl'] = 'w'
rest2.loc[rest2['tokuten'] == rest2["shitten"], 'wdl'] = 'd'
rest2.loc[rest2['tokuten'] < rest2["shitten"], 'wdl'] = 'l'

#1列目のカラム名を「0」から「season」へ変更
rest2.rename(columns={0:'season'}, inplace = True)

#5列目のカラム名を「Unnamed: 5」から「Home&Away」へ変更
rest2.rename(columns={"Unnamed: 5":'Home&Away'}, inplace = True)
rest2 = rest2.reset_index(drop = True)

#結合した際に各シーズンのカラム名の行が残っていたので削除
rest2.drop([33,34,35,36,37,38,39,40,41,42,85,128,171,214,257,300,343,386,429], inplace = True)
pd.set_option('display.max_rows', None)
rest3 = rest2.reset_index(drop = True)

#seasonカラムから年を抽出
rest3["year"] = rest3["season"].apply(lambda x: str(x)[1:5])

#開催日から月と日を分割
rest3['month']  = rest3['開催日'].str.split(pat='.', expand=True)[0]
rest3['day'] = rest3['開催日'].str.split(pat='.', expand=True)[1]

#Home&Awayの変数を数値に変換
rest4 = rest3.replace('A',{'Home&Away':'0',})
rest5 = rest4.replace('H',{'Home&Away':'1',})
display(rest5)

#数値データを日付データに変換
from datetime import datetime
rest5['date'] = pd.to_datetime({'year': rest5['year'], 'month': rest5['month'], 'day': rest5['day']})

#日付昇順に並び替える
rest5 = rest5.sort_values('date', ascending=True)
rest5 = rest5.reset_index(drop = True)
rest5 = rest5.reset_index(drop = True)
display(rest5)

#チャンス構築率、シュート成功率、支配率データの「%」を削除し数値化
rest5["チャンス構築率s"] = rest5["チャンス構築率"].apply(lambda x: str(x)[:-1])
rest5["シュート成功率s"] = rest5["シュート成功率"].apply(lambda x: str(x)[:-1])
rest5["支配率s"] = rest5["支配率"].apply(lambda x: str(x)[:-1])

#「守備P」「シュート」「攻撃 CBP」「パスCBP」「奪取P」「シュート成功率s」「支配率s」「チャンス構築率s」「奪取P」「AGI」「KAGI」の過去3試合の平均を事前情報としてセット。
#(シュート成功率s、支配率s、チャンス構築率sは平均の平均なので使用しないことにしました。)
rest5["守備Pm"] = rest5["守備P"].rolling(window=3).mean().shift(1)
rest5["シュートm"] = rest5["シュート"].rolling(window=3).mean().shift(1)
rest5["攻撃CBPm"] = rest5["攻撃CBP"].rolling(window=3).mean().shift(1)
rest5["パスCBPm"] = rest5["パスCBP"].rolling(window=3).mean().shift(1)
rest5["奪取Pm"] = rest5["奪取P"].rolling(window=3).mean().shift(1)
rest5["シュート成功率m"] = rest5["シュート成功率s"].rolling(window=3).mean().shift(1)
rest5["支配率m"] = rest5["支配率s"].rolling(window=3).mean().shift(1)
rest5["チャンス構築率m"] = rest5["チャンス構築率s"].rolling(window=3).mean().shift(1)
rest5["奪取Pm"] = rest5["奪取P"].rolling(window=3).mean().shift(1)
rest5["AGIm"] = rest5["AGI"].rolling(window=3).mean().shift(1)
rest5["KAGIm"] = rest5["KAGI"].rolling(window=3).mean().shift(1)
display(rest5)

#現時点で必要の無い行、列を削除
rest6 = rest5.dropna(how='any', axis=0)
rest7 = rest6.drop(['season', "開催日", "スコア", "チャンス構築率", "シュート成功率", "支配率", "攻撃CBP", "パスCBP", "奪取P", "守備P", "支配率s", "チャンス構築率s", "シュート成功率s","AGI", "KAGI"], axis=1)
display(rest7)

スクリーンショット 2021-11-28 20.06.05.png

5 取得した情報を元に機械学習モデルを作成

目的変数:wdl(w=勝ち d=負け l=引き分け)
説明変数:Home&Away, 観客数,パスCBPm,攻撃CBPm, 支配率m, AGIm, KAGIm

説明変数の説明(詳しくは「Football LAB(https://www.football-lab.jp/) 」をご参照ください)
*末尾の「m」
パスCBPmなどの末尾の「m」は直近3試合の平均値を表します。
*CBP
選手(またはチーム)が試合を通じてどれだけチャンス機会を構築することができたか
*KAGI
守備の際にどれだけ相手を前進させなかったか、相手を自陣ゴールに近づけなかったか
*AGI
攻撃の際にどれだけ相手ゴールに近づけたか

Aidemyで習ったモデルをいくつか試してみました。

original_columns = ['Home&Away', '観客数','パスCBPm','攻撃CBPm', '支配率m', "AGIm", "KAGIm"]

X_train = rest7[original_columns]

y_train = rest7["wdl"]

# X_train、Y_trainの行数、列数を出力
print(X_train.shape, y_train.shape)

display(X_train.head())
display(y_train.head())

スクリーンショット 2021-11-30 0.16.13.png

from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
logreg = LogisticRegression()
# 交差検証
scores = cross_val_score(logreg, X_train, y_train)
# 各分割におけるスコア
print('Cross-Validation scores: {}'.format(scores))
# スコアの平均値
import numpy as np
print('Average score: {}'.format(np.mean(scores)))

スクリーンショット 2021-11-28 21.58.50.png

from sklearn.ensemble import RandomForestClassifier
from matplotlib import pyplot as plt
import numpy as np

clf = RandomForestClassifier(n_estimators=10,               # 決定木の数
                             criterion='gini',               # 不純度評価指標の種類(ジニ係数)
                             max_depth=10,                    # 木の深さ
                             min_samples_leaf=50,             # 1ノード(葉)の最小クラス数
                             max_features='sqrt')            # 最大特徴量数
clf.fit(X_trn, y_trn)                                    # フィッティング
r2 = clf.score(X_val, y_val)     
print('Test set score: {}'.format(r2))

#from sklearn.metrics import confusion_matrix
#y_preds = .predict(X_trn)
#confusion_matrix(y_trn, y_preds)

from sklearn.model_selection import GridSearchCV

スクリーンショット 2021-11-28 21.58.15.png

線形回帰分析とランダムフォレストを試してみましたが、後者の方が良い結果が出ました。しかし、良い方のモデルであるランダムフォレストのテストデータの正解率が42.9%とまだまだです。特徴量やデータのセット方法などを改善していけば、また違った結果になるのかなと感じました。

6 今後の課題

間違ってるところや、もっと分かりやすくコードを書くべきところがあると思いますが、Aidemyで学んだことを一通りアウトプットでき良かったです。
今後、下記に挑戦してみたいと思います。
・特徴量をさらに増やす
・年間を通した順位予想

今回、プログラミングを学ぶのがほんとんど初めてという状況から開始し、データ分析の楽しさや、難しさを実感する事ができました。今後さらにスキルアップし、精度の高いモデルを作っていきたいです。

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