Jリーグの各クラブのElo Rateを算出してみた

この記事ではJリーグと他のリーグの比較はしていません。 あくまでJリーグ内で各クラブの比較になります。

イロレーティング

イロレーティング (Elo rating) とは、対戦型の競技(2人のプレイヤーまたは2つのチームが対戦して勝敗を決めるタイプの競技)において、相対評価で実力を表すために使われる指標の一つ。数学的裏付けのある最も著名なレーティングシステムである。

イロレーティングは、もともとチェスの実力を表すために考案されたものだが、様々な競技に応用されている。具体的には

国際チェス連盟の公式記録 日本アマチュア将棋連盟の公式記録 将棋や囲碁などのオンライン対局場 サッカーのFIFAランキング ラグビーなどの一部の競技団体のランキング 対戦型オンラインゲームのランキングやマッチング などでイロレーティング、あるいはイロレーティングを改変したレーティングシステムが採用されている。一部の競技では単にレーティングと呼ぶこともある。

なお、「イロ」とは、考案者であるアルパド・イロ(ハンガリー生まれのアメリカ人物理学者)に由来する。

FROM WIKI

イロレーティング - Wikipedia

ざっくり言ったら強さの相対評価指標で対戦相手同士の数値を比較し、期待勝率を求めることができます。 計算式はこんな感じらしいです。

 R_a = R_b + K \cdot K_r \cdot (W_r - W_e)

式に出てくる文字について

 R_a :計算後のレート

 R_b :計算前のレート

 K :加減値となる定数 リーグ戦とかでは30で設定されることが多いらしいです。

 K_r:点差による掛け率です。 サッカーなら1点差以内:1、2点差:1.5、3点差:1.75、4点差:1.875、以降N点差2+(N-3)/8とするそうです。

KとKrについてはWorld Football Elo Ratingsより

www.eloratings.net

 W_r: 勝敗結果を数値化したものです。

勝ち:1、引き分け:0.5、負け:0

 W_e:Ratingの差による期待勝率です。計算式は以下のようになっています。

 W_e = \dfrac {1}{1 + (10 ^  {- \frac {dir} {400}})}

dirはそれぞれ対戦相手とのレートの差となっています。

例えば、レートが1700のチームAとレートが1500のチームBが対戦した場合、  dir = 200となり、

 W_e = \dfrac {1}{1 + (10 ^  {- \frac {200} {400}})}  = \frac {1}{1 + (10 ^  {- \frac {1} {2}})} = 0.7597 となります。

つまり、チームAはチームBに対して勝率76%を期待でき、逆にチームBはチームAに対して24%の勝率が見込めると言うものです。

レートに差がない時はお互いに五分五分の勝率を期待できます。

これが期待勝率[tx:W_e]となります。

ちなみに、Elo Rateですが、対戦相手による得意不得意などの相性はないと言う前提で計算しています。 これが現実の試合に当てはまるかと言ったら微妙ですけどね。

まぁElo Rateの説明は以上として、実際にどんな感じで計測したかと結果を書きたいと思います。

実装

Jリーグの対戦成績を全部集めるの面倒だったので2021年〜2023年のデータを取得しました。

取得方法はこちらのブログの記事を参考にしてスクレイピングを行いました。 unhat-lab.com

スクレイピングしたデータはそれぞれdf_stats_j_year.csvで出力しました。

出力したファイルを使い、Elo Rateを求めていきたいと思います。

j1_2021_rs = pd.read_csv('df_stats_j1_2021.csv')
j1_2022_rs = pd.read_csv('df_stats_j1_2022.csv')
j1_2023_rs = pd.read_csv('df_stats_j1_2023.csv')
j2_2021_rs = pd.read_csv('df_stats_j2_2021.csv')
j2_2022_rs = pd.read_csv('df_stats_j2_2022.csv')
j2_2023_rs = pd.read_csv('df_stats_j2_2023.csv')
j3_2021_rs = pd.read_csv('df_stats_j3_2021.csv')
j3_2022_rs = pd.read_csv('df_stats_j3_2022.csv')
j3_2023_rs = pd.read_csv('df_stats_j3_2023.csv')

Elo Rateを算出するために、まずチーム名とレートを管理するteamテーブルと 試合結果を管理するresultテーブルを作成しました。

teamテーブル

#j1
team_j1_TB = j1_2021_rs.loc[:,['チーム名']].drop_duplicates()
team_j1_TB['No'] = range(1, len(team_j1_TB) + 1)
team_j1_TB = team_j1_TB.loc[:,['No','チーム名']]
team_j1_TB['Rate'] = 1500
team_j1_TB['Rate_max'] = 1500
team_j1_TB['Date'] = '2021-01-01'
team_j1_TB = team_j1_TB.rename(columns={'チーム名': 'Team_name'})

J2、J3においても同様の処理をしました。 Rateは現在のレート、Rate_maxはレートが最大となった時の数値。

Dateはレートが最大となった日を記録するための項目です。

Noは何チームあるかぱっと見でわかるようにしただけの項目です。 また、初期のレートですが、2021年時点で各カテゴリに所属しているチームに以下の値を設定しました。

J1:1500、J2:1400、J3:1300

そして、2021年時点ではjリーグに参加していない「いわきFC」「奈良クラブ」「FC大阪の」の3チームに関しては初期レートを1250で設定しました。

最終的には収束するらしいので初期値はなんでもいいらしいですが、この辺は完全に個人的感覚で設定しています。

new_team = pd.DataFrame({'No': ['58', '59', '60'],
                   'Team_name': ['いわきFC', '奈良クラブ', 'FC大阪'],
                   'Rate': [1250, 1250, 1250],
                   'Rate_max': [1250, 1250, 1250],
                   'Date':['2022-01-01','2023-01-01','2023-01-01']})
#new_team
team_j3_TB = team_j3_TB.append(new_team)
#結合
team_TB = pd.concat([team_j1_TB,team_j2_TB,team_j3_TB])
team_TB

結果、このようなテーブルが作成されました。

続いて、resultテーブルですが、欲しい情報はチーム名、対戦相手の名前、試合の日付、両チームの得点数ですので、 次のようにしてデータを結合しました。

スクレイピングしたデータでは第Ν節のAvsBとBvsAのデータが重複して入っているため、 重複を消す処理をしています。

また、時系列順にElo Rateが変化して欲しいので日付順でソートしています。

j_2021_rs = pd.concat([j1_2021_rs,j2_2021_rs,j3_2021_rs]).drop_duplicates(subset=['日付', '場所']).sort_values('日付')
j_2021_rs = j_2021_rs[['日付','チーム名','対戦相手','得点','失点']]
j_2021_rs = j_2021_rs.rename(columns={'日付':'Date','チーム名': 'Home_team','対戦相手':'Away_team','得点':'Home_Goal','失点':'Away_Goal'})

#2022
j_2022_rs = pd.concat([j1_2022_rs,j2_2022_rs,j3_2022_rs]).drop_duplicates(subset=['日付', '場所']).sort_values('日付')
j_2022_rs = j_2022_rs[['日付','チーム名','対戦相手','得点','失点']]
j_2022_rs = j_2022_rs.rename(columns={'日付':'Date','チーム名': 'Home_team','対戦相手':'Away_team','得点':'Home_Goal','失点':'Away_Goal'})
#2023
j_2023_rs = pd.concat([j1_2023_rs,j2_2023_rs,j3_2023_rs]).drop_duplicates(subset=['日付', '場所']).sort_values('日付')
j_2023_rs = j_2023_rs[['日付','チーム名','対戦相手','得点','失点']]
j_2023_rs = j_2023_rs.rename(columns={'日付':'Date','チーム名': 'Home_team','対戦相手':'Away_team','得点':'Home_Goal','失点':'Away_Goal'})
#2021~2023を結合
j_rs = pd.concat([j_2021_rs,j_2022_rs,j_2023_rs])

これで欲しいデータは揃ったので、あとは計算していくだけです。

Elo Rateについては冒頭の式に基づき、以下のように関数を作成しました。

def Elo_Rate(Rate_Home,Rate_Away,Goal_Home,Goal_Away,K = 30):
    
    #勝敗結果
    dif  = Goal_Home - Goal_Away
    
    if dif > 0:
        W = 1
    elif dif < 0:
        W = 0
    else :
        W = 0.5

    #点差による係数Krの算出
    dif_abs  = abs(Goal_Home - Goal_Away)
    
    if dif_abs <= 1:
        Kr = 1
    elif dif_abs == 2:
        Kr = 1.5
    elif dif_abs == 3:
        Kr = 1.75
    elif dif_abs == 4:
        Kr = 1.875
    else:
        Kr = 1.75 + ((dif_abs - 5)/16)
        
    #Ratingの差を求め,Weを計算
    dr = float(Rate_Home  - Rate_Away)
    
    We = 1 / (10 ** (-dr/400) + 1)
    
    Rating = K * Kr *(W - We)

    #Elo_Rateの算出
    Elo_Rate_H = round(Rate_Home + Rating,2)
    Elo_Rate_A = round(Rate_Away + Rating * (-1),2)
    
    return (Elo_Rate_H, Elo_Rate_A)

こちらの関数をもとにループさせていきます。 色々Kの値を試した結果、25が良さそうだったのでK=25で実行しています。

tqdmてのはプログレスバーを表示させるだけですので無くても実行に問題はありません。

team_TB = pd.concat([team_j1_TB,team_j2_TB,team_j3_TB])

for i in tqdm(range(len(j_rs))):
    K = 25
    
    Home_Team = j_rs.iat[i,1]
    Away_Team = j_rs.iat[i,2]
    Home_Goal = j_rs.iat[i,3]
    Away_Goal = j_rs.iat[i,4]
    Home_Rate_Pre = team_TB[team_TB['Team_name'] == Home_Team]['Rate']
    Home_Rate = float(Home_Rate_Pre.iloc[-1])
    Away_Rate_Pre = team_TB[team_TB['Team_name'] == Away_Team]['Rate']
    Away_Rate = float(Away_Rate_Pre.iloc[-1])
    
    #Elo_Rate callcurate
    Home_Rate_After  = Elo_Rate(Home_Rate,Away_Rate,Home_Goal,Away_Goal,K)[0] 
    Away_Rate_After = Elo_Rate(Home_Rate,Away_Rate,Home_Goal,Away_Goal,K)[1] 
    
    if Home_Rate_After >= team_TB.loc[team_TB['Team_name'] == Home_Team,'Rate_max'].iloc[-1]:
        team_TB.loc[team_TB['Team_name'] == Home_Team,'Rate_max'] = Home_Rate_After
        team_TB.loc[team_TB['Team_name'] == Home_Team,'Date'] = j_rs.iat[i,0]
    if Away_Rate_After >= team_TB.loc[team_TB['Team_name'] == Away_Team,'Rate_max'].iloc[-1]:
        team_TB.loc[team_TB['Team_name'] == Away_Team,'Rate_max'] = Away_Rate_After
        team_TB.loc[team_TB['Team_name'] == Away_Team,'Date'] = j_rs.iat[i,0]

    team_TB.loc[team_TB['Team_name'] == Home_Team,'Rate'] = Home_Rate_After
    team_TB.loc[team_TB['Team_name'] == Away_Team,'Rate'] = Away_Rate_After

team_TB['Ranking']=team_TB['Rate'].rank(ascending=False)
team_TB = team_TB[['Team_name','Rate','Rate_max','Date','Ranking']]
team_TB = team_TB.rename(columns={'Date':'Rate_max_date'})

結果は以下のようになりました。 個人的には割と妥当な気がしています。

気になる点は町田の順位が高いのを筆頭に、上のカテゴリで苦戦しているチームより下のカテゴリで勝っているチームの方が数字が上がりやすいところですね。

Jリーグは1部と2部の差がそこまであるリーグじゃないと思いますし、今年1位で昇格した新潟は夏に伊藤涼太郎が抜けたけど最終的には10位でしたのでJ2で首位のチームならまぁ上の方に来てもいいんじゃないかなと思います。

あとガンバが数字上だとめちゃくちゃ弱いですw。 降格した横浜FCより弱いみたいですね。 来シーズン大丈夫でしょうか。

Rateの最大値で見ると2023年7月のマリノス、2021年10月の川崎、2023年最終節の神戸がTOP3みたいでこちらも個人的には川崎のが上だろと思いましたがまぁ数値的にほぼ差がないので納得てところです。

統計検定2級に合格した話

私は2020年8月にCBT方式で試験を受けました。

点数はたしか84点とかでした。勉強期間は2ヶ月くらいでした。

勉強方法は主に過去問を解くこと、サイトを参照すること、統計検定以外の統計学の問題を解くことです。

結論から言うならば、2級に合格するだけなら過去問を解くだけでいいと思います。2級の合格点は60点ですので過去問の回数をこなせば60点を超えることはそこまで難易度の高いことではないのかなと思っています。(過去問から全く同じ問題が出るということはないですが、似たような問題は結構でます)しかし、2級で高得点を狙いたい、準1級や1級をこの先合格したいというならば、統計学や数学の勉強をしっかりして、数理統計の原理を抑えることがマストなのではないかと思います。

実際私は、今準1級の勉強をしていますが、2級とはレベルが違います。

2級レベルの問題もありますが、2級を9割以上取れるレベルの人じゃないと厳しいと思います。

私も2級は8割程度しか取れていなかったため、準1級の問題に苦しんでいるところです。

 

参考までに私が使ったテキストとサイトがこちらです。

bellcurve.jp

 

 統計Webに過去問と解説が載っているので、公式の過去問を買う必要が絶対あるわけではないです。私は自分の手元にテキスト置いて、間違えた問題にチェックを付けたりしたかったので、買いました。

 大学の統計学は、丁寧に統計数理の基本を書いており演習用の問題もあるため、おすすめです。合格するだけなら過去問をひたすら解くだけでいいので、大学の統計学はなくても合格はできると思います。

 また、統計学を理解するためには数学がマストなので、大学数学の線形代数についても勉強したほうが確実にレベルアップにつながるのではないかと考えられます。大学数学を勉強するなら、マセマのテキストがおすすめです。メルカリでよく流通してます。

www.mathema.jp

 

まあ、冒頭にも述べたように2級をとるだけなら過去問をひたすら解くだけでいいと思うんで、より高みに行きたい人は数学も勉強したほうがいいよねって話でした。以上、そんな高みにいるレベルの人間じゃないのに上から目線になって語ってしまいました。

 

準1級もCBT方式が始まったみたいなので合格したいと思います。

Pandasで特定の列以外を取得して説明変数にしたい時の備忘録

教師あり学習をしたいときなんかがあります。 その時は目的変数のベクトルをyとかにして、説明変数の行列をXとかにしたい場合が多いと思います。 そういった時にnumpyと違って指定がめんどくさいpandasさんで簡単にXとyを指定する方法がこれです。

trainというデータテーブルがあり、その中のy列を目的変数yに、それ以外の列を説明変数Xとしています。

~
X = train.drop("y", axis=1)
y = train['y']
~

drop関数は引数inplaceをTrueにしなければ元のテーブルデータが維持されるので安全に使えますね。

PythonのNumpyを使いこなす練習

データサイエンティストとしてPythonを使うにあたって、Numpyを使いこなすことは必至です。 ですので、その練習をしようと練習用のサイトを探しました。日本語サイトがよかったのですが、日本語ではいいのは見つからなくて、英語の方で探していると、丁度いいのがありました。

このサイトです。

www.machinelearningplus.com

101 NumPy Exercises for Data Analysis (Python) というタイトルで2018年に作成されたようです。

 

f:id:K_80EYE:20210529135308p:plain
サイトのトップ画

私は問題を解く際にJupyter  Notebookを使いました。 Jupyter Notebook のインストールは他のサイトを参照してください。「Jupyter Notebook インストール」とかで検索したらいっぱい出てきます。

代表例として下のサイト置いておきます。

udemy.benesse.co.jp

 

実際の問題はこんな感じです。

f:id:K_80EYE:20210529135523p:plain
問題例

inputaにて問題で使う要素が与えられ、outputが出力したい結果です。 solutionをクリックすれば回答を見ることができます。 

この問題を私はこんな感じで解きました。 答えにたどり着く方法は、他にもいっぱいあると思います。

a = np.arange(10).reshape(2,-1)
b = np.repeat(1,10).reshape(2,-1)

print('a = \n',a)
print('b = \n',b)
print(np.concatenate([a,b],1))

基本的にはこんな感じで70個の問題を解きました。

いくつか質問の意図がわかり辛い問題もあって、その時は答えを見て質問の意図を理解して別の方法で答えと同じものを出力しました。

私は初心者ですが、ちょうどいい難易度と量だった思います。

次はPandasやplotにも挑戦してみたいです。

【初投稿】ブログをはじめようと思ったきっかけ

はじめまして、K_80EYEです。

まず、記事のタイトルの答えを書きたいと思います。

Q.どうしてブログを始めたの?

A.データサイエンティストとして働くにあたって、アウトプットするための場所が欲しかったから。

すいません、わかりづらいですね… 

まず、僕は今年の春からデータサイエンティストとして働くことになりました。現在、会社の研修を受けており、Pythonの勉強中なのですが、これアウトプットしておかないと自分の身につかないな…と思い、ブログを始めることにしました。自分が処理に困ったことや工夫したこと、エラーの解決とか書き残していきたいと思います。

このブログは、自分の勉強用つもりなので、不備が多かったり間違ってる点も多々あるかもしれないですが、目をつむってやってください。

 

後は、自分の経歴とか趣味について書きたいと思います。

 

[自分の経歴について]

大学は一応関西で一番の私立大学を出ました。大学ではデータサイエンスとか統計学とかその辺の勉強をしてました。卒業した後は、就職しなかった(できなかった)ため、半年ほど大学院目指して勉強してました。しかし、こちらの大学院受験もうまく行かず、12月くらいから就活始めました。(既卒でこの時期から就活始めたので、滅茶苦茶しんどかったです。)すると、データサイエンティストを募集している求人を見つけたので、そこに応募して何とか就職先決まって今に至る形です。

[大学時代に学んだこと]

  • 統計解析(主にRを使って)
  • 情報系の基礎的なこと 
  • 統計検定2級合格のための勉強(後に合格体験記的なの書きたいと思います。)

[趣味]

  • サッカー(高校までサッカー部でした、今は見る専です。)
  • 漫画
  • ゲーム(ボードゲームとデジタルカードゲーム)
  • ダイエット(今年こそ瘦せなければ)