kaggleチャレンジ 14日目 FIFAワールドカップの結果を予測してみる 

シェアする

  • このエントリーをはてなブックマークに追加
スポンサーリンク

はじめに

  今回は、kaggleのデータセットにあったFIFA Soccer Ranking というデータセットを使っていきたいと思います。このデータセットは、1993年8月から2018年4月までのFIFA男子国際サッカーランキングがすべて掲載されています。このデータセットを使って2018年のFIFAワールドカップの結果を予測し、答え合わせをしてみたいと思います。

今回使うデータセット

合わせて読みたい記事:

今回もGoogleColaboratoryを使って進めていくので、はじめ方などは前回の記事を参考にしてください。

データセットの説明

1993年8月から2018年4月までのFIFA男子国際サッカーランキングが全て入っている。

ランキングの詳しい説明と歴史については、こちらを参照:

https://en.wikipedia.org/wiki/FIFA_World_Rankings

ランク付け手順についてはこちらを参照:

https://www.fifa.com/fifa-world-ranking/procedure/men.html

すべてのチームと1993-2018の利用可能なデータが含まれている。

データセットの内訳

FIFAのランキングページ(https://www.fifa.com/fifa-world-ranking/ranking-table/men/index.html)で利用可能なデータ。

  • ランク
  • 国名略称
  • 合計点
  • 前のポイント
  • ランク変更
  • 前年度の平均ポイント
  • 前年度の加重平均ポイント(50%)
  • 平均2年間のポイント
  • 2年前の加重平均ポイント(30%)
  • 平均3年間のポイント
  • 3年前の加重平均ポイント(20%)
  • 連合

FIFA Soccer Rankings

 今回はこちらのカーネルを参考にして進めていきたいと思います。

2018年FIFAワールドカップの勝者を予測

方法

ランク、ポイント、および相手との加重点の違いを考慮して、データを使用してチーム間のペアリングの結果をモデル化する。
このモデルを使用して、グループラウンドの結果を予測し、その後、シングル・エリミネーション。シングル・エリミネーションは、プレイヤーが1つのマッチに負けたらトーナメントから除外されるトーナメント形式(いわゆる勝ち抜き戦)のこと。

いくつかの注意点

  • エジプトはベスト16、ポルトガルとの試合はフェーズで最も安全な賭けだ。
  • このシミュレーションでは、ブラジルはベルギーを通過できません。
  • C.ロナウド(POR)もL.メッシ(ARG)も決勝では出場しないようですが、準々決勝では互いに対戦するつもりらしい。

データ入出力

  • 1993年から2018年のFIFAランキング(Tadhg Fitzgerald
    これはFIFAランキングとチームのポイントを取得するために使用したものです。
  • 1872年から2018年までの国際サッカーの試合
    これはポイント、ランク、チームの現在のランクの差が試合にどれほど影響するかを調べるために使用します。
  • FIFAワールドカップ2018データセット
    次の試合を得るためにこれを使用します。
    *データファイルを読むときは、ソースの不一致があるので、国名の変更に注意します。

3つのデータセット

In[1]:

import numpy as np # linear algebra
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)
from matplotlib import pyplot as plt

rankings = pd.read_csv('fifa_ranking.csv')
rankings = rankings.loc[:,['rank', 'country_full', 'country_abrv', 'cur_year_avg_weighted', 'rank_date', 
                           'two_year_ago_weighted', 'three_year_ago_weighted']]
rankings = rankings.replace({"IR Iran": "Iran"})
rankings['weighted_points'] =  rankings['cur_year_avg_weighted'] + rankings['two_year_ago_weighted'] + rankings['three_year_ago_weighted']
rankings['rank_date'] = pd.to_datetime(rankings['rank_date'])

matches = pd.read_csv('results.csv')
matches =  matches.replace({'Germany DR': 'Germany', 'China': 'China PR'})
matches['date'] = pd.to_datetime(matches['date'])

world_cup = pd.read_csv('World Cup 2018 Dataset.csv')
world_cup = world_cup.loc[:, ['Team', 'Group', 'First match \nagainst', 'Second match\n against', 'Third match\n against']]
world_cup = world_cup.dropna(how='all')
world_cup = world_cup.replace({"IRAN": "Iran", 
                               "Costarica": "Costa Rica", 
                               "Porugal": "Portugal", 
                               "Columbia": "Colombia", 
                               "Korea" : "Korea Republic"})
world_cup = world_cup.set_index('Team')

ranking
  • イランの表記を置き換えた。
  • それぞれの加重平均ポイントを足し合わせて、加重平均のカラムを作った。
  • rankings[‘rank_date’]内の文字列をdatetime64型(Timestamp型)に変換
matches
  • ドイツと中国の表記を置き換えた。
  • matches[‘rank_date’]内の文字列をdatetime64型(Timestamp型)に変換
world_cup
  • 欠損値の削除(how = ‘all’ : もし全ての値がNAなら削除)。
  • 国名の表記を置き換えた。
  • world_cupの[‘Team’]をindex(行名、行ラベル)に割り当てた。

特徴抽出

異なるチームのランクとの試合に参加します。次に、いくつかの特徴を抽出します。

ポイントとランクの違い
友好的な試合が予測するのが難しいということだったので(TODOは残りのWC試合を区別していた)各チームが何日間休むことができたのかは分かりましたが、これは重要ではないとわかりました。

In[2]:

# I want to have the ranks for every day 
rankings = rankings.set_index(['rank_date'])\
            .groupby(['country_full'], group_keys=False)\
            .resample('D').first()\
            .fillna(method='ffill')\
            .reset_index()

# join the ranks
matches = matches.merge(rankings, 
                        left_on=['date', 'home_team'], 
                        right_on=['rank_date', 'country_full'])
matches = matches.merge(rankings, 
                        left_on=['date', 'away_team'], 
                        right_on=['rank_date', 'country_full'], 
                        suffixes=('_home', '_away'))
ranking
  • ranking[‘rank_date’]を行ラベルにセット
  • ranking[‘country_full’]に従い、グループ分け
  • resample は周期ごとにサンプリングしなおす。’D’は日次
  • 欠損値は見つけたとき、最後に有効だった値に置き換える(method=‘ffill’)
matches
  • rankingとマージしている
In [3]:
# feature generation
matches['rank_difference'] = matches['rank_home'] - matches['rank_away']
matches['average_rank'] = (matches['rank_home'] + matches['rank_away'])/2
matches['point_difference'] = matches['weighted_points_home'] - matches['weighted_points_away']
matches['score_difference'] = matches['home_score'] - matches['away_score']
matches['is_won'] = matches['score_difference'] > 0 # take draw as lost
matches['is_stake'] = matches['tournament'] != 'Friendly'

max_rest = 30
matches['rest_days'] = matches.groupby('home_team').diff()['date'].dt.days.clip(0,max_rest).fillna(max_rest)

matches['wc_participant'] = matches['home_team'] * matches['home_team'].isin(world_cup.index.tolist())
matches['wc_participant'] = matches['wc_participant'].replace({'':'Other'})
matches = matches.join(pd.get_dummies(matches['wc_participant']))

  • matchesに新たなカラムを作り、追加した。([‘rank_difference’],[‘average_rank’],[‘point_difference’],[‘score_difference’],[‘is_won’],[‘is_stake’][‘rest_days’] )
  • matches[‘wc_participant’]はダミー変数に変更後追加した。

モデリング

学習用データと検証用データにわけた。カーネルでは、シンプルなロジスティック回帰で行なっている。目的変数は[‘is_win’]ということで、勝ったかどうかですね。学習させたのち、予測したデータからAUCスコアをだした。

In[4]:

from sklearn import linear_model
from sklearn import ensemble
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix, roc_curve, roc_auc_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures

X, y = matches.loc[:,['average_rank', 'rank_difference', 'point_difference', 'is_stake']], matches['is_won']
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42)

logreg = linear_model.LogisticRegression(C=1e-5)
features = PolynomialFeatures(degree=2)
model = Pipeline([
    ('polynomial_features', features),
    ('logistic_regression', logreg)
])
model = model.fit(X_train, y_train)

# figures 
fpr, tpr, _ = roc_curve(y_test, model.predict_proba(X_test)[:,1])
plt.figure(figsize=(15,5))
ax = plt.subplot(1,3,1)
ax.plot([0, 1], [0, 1], 'k--')
ax.plot(fpr, tpr)
ax.set_title('AUC score is {0:0.2}'.format(roc_auc_score(y_test, model.predict_proba(X_test)[:,1])))
ax.set_aspect(1)

ax = plt.subplot(1,3,2)
cm = confusion_matrix(y_test, model.predict(X_test))
ax.imshow(cm, cmap='Blues', clim = (0, cm.max())) 

ax.set_xlabel('Predicted label')
ax.set_title('Performance on the Test set')

ax = plt.subplot(1,3,3)
cm = confusion_matrix(y_train, model.predict(X_train))
ax.imshow(cm, cmap='Blues', clim = (0, cm.max())) 
ax.set_xlabel('Predicted label')
ax.set_title('Performance on the Training set')
pass

悪い予測結果を見て、どこでミスを頻繁にしているのかを見てみます。

In[5]:

features = ['average_rank', 'rank_difference', 'point_difference']
wrongs = y_test != model.predict(X_test)

for feature in features:
    plt.figure()
    plt.title(feature)
    X_test.loc[wrongs, feature].plot.kde()
    X.loc[:, feature].plot.kde()
    plt.legend(['wrongs', 'all'])
    
print("Stakes distribution in the wrong predictions")
print(X_test.loc[wrongs, 'is_stake'].value_counts() / wrongs.sum())
print("Stakes distribution overall")
print(X['is_stake'].value_counts() / X.shape[0])

わかったこと

  • ランクが近いものに対して予測結果が悪化している
  • 一般的に下位のランク
  • 賭けていない試合のためにも幾分か(ここではフレンドリー)
  • 幸運なことに、これは、ワールドカップのために、我々の予測は幾分良い

ワールドカップシミュレーション

グループラウンド

In[6]:

# let's define a small margin when we safer to predict draw then win
margin = 0.05

# let's define the rankings at the time of the World Cup
world_cup_rankings = rankings.loc[(rankings['rank_date'] == rankings['rank_date'].max()) & 
                                    rankings['country_full'].isin(world_cup.index.unique())]
world_cup_rankings = world_cup_rankings.set_index(['country_full'])
In [7]:
from itertools import combinations

opponents = ['First match \nagainst', 'Second match\n against', 'Third match\n against']

world_cup['points'] = 0
world_cup['total_prob'] = 0

for group in set(world_cup['Group']):
    print('___Starting group {}:___'.format(group))
    for home, away in combinations(world_cup.query('Group == "{}"'.format(group)).index, 2):
        print("{} vs. {}: ".format(home, away), end='')
        row = pd.DataFrame(np.array([[np.nan, np.nan, np.nan, True]]), columns=X_test.columns)
        home_rank = world_cup_rankings.loc[home, 'rank']
        home_points = world_cup_rankings.loc[home, 'weighted_points']
        opp_rank = world_cup_rankings.loc[away, 'rank']
        opp_points = world_cup_rankings.loc[away, 'weighted_points']
        row['average_rank'] = (home_rank + opp_rank) / 2
        row['rank_difference'] = home_rank - opp_rank
        row['point_difference'] = home_points - opp_points
        
        home_win_prob = model.predict_proba(row)[:,1][0]
        world_cup.loc[home, 'total_prob'] += home_win_prob
        world_cup.loc[away, 'total_prob'] += 1-home_win_prob
        
        points = 0
        if home_win_prob <= 0.5 - margin:
            print("{} wins with {:.2f}".format(away, 1-home_win_prob))
            world_cup.loc[away, 'points'] += 3
        if home_win_prob > 0.5 - margin:
            points = 1
        if home_win_prob >= 0.5 + margin:
            points = 3
            world_cup.loc[home, 'points'] += 3
            print("{} wins with {:.2f}".format(home, home_win_prob))
        if points == 1:
            print("Draw")
            world_cup.loc[home, 'points'] += 1
            world_cup.loc[away, 'points'] += 1

Out[7]:

___Starting group A:___
Russia vs. Saudi Arabia: Saudi Arabia wins with 0.55
Russia vs. Egypt: Egypt wins with 0.66
Russia vs. Uruguay: Uruguay wins with 0.83
Saudi Arabia vs. Egypt: Egypt wins with 0.65
Saudi Arabia vs. Uruguay: Uruguay wins with 0.82
Egypt vs. Uruguay: Uruguay wins with 0.74
___Starting group H:___
Poland vs. Senegal: Poland wins with 0.60
Poland vs. Colombia: Draw
Poland vs. Japan: Poland wins with 0.73
Senegal vs. Colombia: Colombia wins with 0.59
Senegal vs. Japan: Senegal wins with 0.65
Colombia vs. Japan: Colombia wins with 0.70
___Starting group C:___
France vs. Australia: France wins with 0.65
France vs. Peru: Draw
France vs. Denmark: Draw
Australia vs. Peru: Peru wins with 0.67
Australia vs. Denmark: Denmark wins with 0.70
Peru vs. Denmark: Draw
___Starting group E:___
Brazil vs. Switzerland: Draw
Brazil vs. Costa Rica: Brazil wins with 0.62
Brazil vs. Serbia: Brazil wins with 0.67
Switzerland vs. Costa Rica: Switzerland wins with 0.59
Switzerland vs. Serbia: Switzerland wins with 0.65
Costa Rica vs. Serbia: Draw
___Starting group G:___
Belgium vs. Panama: Belgium wins with 0.73
Belgium vs. Tunisia: Belgium wins with 0.61
Belgium vs. England: Belgium wins with 0.57
Panama vs. Tunisia: Tunisia wins with 0.73
Panama vs. England: England wins with 0.77
Tunisia vs. England: England wins with 0.57
___Starting group B:___
Portugal vs. Spain: Draw
Portugal vs. Morocco: Portugal wins with 0.68
Portugal vs. Iran: Portugal wins with 0.67
Spain vs. Morocco: Spain wins with 0.65
Spain vs. Iran: Spain wins with 0.64
Morocco vs. Iran: Draw
___Starting group F:___
Germany vs. Mexico: Germany wins with 0.60
Germany vs. Sweden: Germany wins with 0.63
Germany vs. Korea Republic: Germany wins with 0.71
Mexico vs. Sweden: Draw
Mexico vs. Korea Republic: Mexico wins with 0.69
Sweden vs. Korea Republic: Sweden wins with 0.64
___Starting group D:___
Argentina vs. Iceland: Argentina wins with 0.60
Argentina vs. Croatia: Argentina wins with 0.59
Argentina vs. Nigeria: Argentina wins with 0.70
Iceland vs. Croatia: Draw
Iceland vs. Nigeria: Iceland wins with 0.62
Croatia vs. Nigeria: Croatia wins with 0.63

シングル・エリミネーションラウンド

In[8]:

pairing = [0,3,4,7,8,11,12,15,1,2,5,6,9,10,13,14]

world_cup = world_cup.sort_values(by=['Group', 'points', 'total_prob'], ascending=False).reset_index()
next_round_wc = world_cup.groupby('Group').nth([0, 1]) # select the top 2
next_round_wc = next_round_wc.reset_index()
next_round_wc = next_round_wc.loc[pairing]
next_round_wc = next_round_wc.set_index('Team')

finals = ['round_of_16', 'quarterfinal', 'semifinal', 'final']

labels = list()
odds = list()

for f in finals:
    print("___Starting of the {}___".format(f))
    iterations = int(len(next_round_wc) / 2)
    winners = []

    for i in range(iterations):
        home = next_round_wc.index[i*2]
        away = next_round_wc.index[i*2+1]
        print("{} vs. {}: ".format(home,
                                   away), 
                                   end='')
        row = pd.DataFrame(np.array([[np.nan, np.nan, np.nan, True]]), columns=X_test.columns)
        home_rank = world_cup_rankings.loc[home, 'rank']
        home_points = world_cup_rankings.loc[home, 'weighted_points']
        opp_rank = world_cup_rankings.loc[away, 'rank']
        opp_points = world_cup_rankings.loc[away, 'weighted_points']
        row['average_rank'] = (home_rank + opp_rank) / 2
        row['rank_difference'] = home_rank - opp_rank
        row['point_difference'] = home_points - opp_points

        home_win_prob = model.predict_proba(row)[:,1][0]
        if model.predict_proba(row)[:,1] <= 0.5:
            print("{0} wins with probability {1:.2f}".format(away, 1-home_win_prob))
            winners.append(away)
        else:
            print("{0} wins with probability {1:.2f}".format(home, home_win_prob))
            winners.append(home)

        labels.append("{}({:.2f}) vs. {}({:.2f})".format(world_cup_rankings.loc[home, 'country_abrv'], 
                                                        1/home_win_prob, 
                                                        world_cup_rankings.loc[away, 'country_abrv'], 
                                                        1/(1-home_win_prob)))
        odds.append([home_win_prob, 1-home_win_prob])
                
    next_round_wc = next_round_wc.loc[winners]
    print("\n")
Out[8]:
___Starting of the round_of_16___
Uruguay vs. Spain: Spain wins with probability 0.54
Denmark vs. Croatia: Denmark wins with probability 0.55
Brazil vs. Mexico: Brazil wins with probability 0.58
Belgium vs. Colombia: Belgium wins with probability 0.59
Egypt vs. Portugal: Portugal wins with probability 0.81
France vs. Argentina: Argentina wins with probability 0.53
Switzerland vs. Germany: Germany wins with probability 0.60
England vs. Poland: Poland wins with probability 0.53


___Starting of the quarterfinal___
Spain vs. Denmark: Denmark wins with probability 0.51
Brazil vs. Belgium: Belgium wins with probability 0.51
Portugal vs. Argentina: Portugal wins with probability 0.51
Germany vs. Poland: Germany wins with probability 0.57


___Starting of the semifinal___
Denmark vs. Belgium: Belgium wins with probability 0.57
Portugal vs. Germany: Germany wins with probability 0.56


___Starting of the final___
Belgium vs. Germany: Germany wins with probability 0.54

結果を可視化

In[9]:

import networkx as nx
import pydot
from networkx.drawing.nx_pydot import graphviz_layout

node_sizes = pd.DataFrame(list(reversed(odds)))
scale_factor = 0.3 # for visualization
G = nx.balanced_tree(2, 3)
pos = graphviz_layout(G, prog='twopi', args='')
centre = pd.DataFrame(pos).mean(axis=1).mean()

plt.figure(figsize=(10, 10))
ax = plt.subplot(1,1,1)
# add circles 
circle_positions = [(235, 'black'), (180, 'blue'), (120, 'red'), (60, 'yellow')]
[ax.add_artist(plt.Circle((centre, centre), 
                          cp, color='grey', 
                          alpha=0.2)) for cp, c in circle_positions]

# draw first the graph
nx.draw(G, pos, 
        node_color=node_sizes.diff(axis=1)[1].abs().pow(scale_factor), 
        node_size=node_sizes.diff(axis=1)[1].abs().pow(scale_factor)*2000, 
        alpha=1, 
        cmap='Reds',
        edge_color='black',
        width=10,
        with_labels=False)

# draw the custom node labels
shifted_pos = {k:[(v[0]-centre)*0.9+centre,(v[1]-centre)*0.9+centre] for k,v in pos.items()}
nx.draw_networkx_labels(G, 
                        pos=shifted_pos, 
                        bbox=dict(boxstyle="round,pad=0.3", fc="white", ec="black", lw=.5, alpha=1),
                        labels=dict(zip(reversed(range(len(labels))), labels)))

texts = ((10, 'Best 16', 'black'), (70, 'Quarter-\nfinal', 'blue'), (130, 'Semifinal', 'red'), (190, 'Final', 'yellow'))
[plt.text(p, centre+20, t, 
          fontsize=12, color='grey', 
          va='center', ha='center') for p,t,c in texts]
plt.axis('equal')
plt.title('Single-elimination phase\npredictions with fair odds', fontsize=20)
plt.show()

この表示の仕方いいですね。予測結果では、決勝がドイツ対ベルギー戦になり、ドイツが1位となりました。サッカー詳しくないのでよさそうな結果なのかあまりわからなかったので、世界ランクをのぞいてみました。FIFAワールドカップ2018の開催期間が2018年6月14日から2018年7月16日ということだったので、2018年6月時点のデータを見たところ…

ドイツ1位、ブラジル2位、ベルギー3位(FIFAランキング 2018年6月時点)

順位通りといった感じですね。

では、実際はどうだったのでしょうか?

(https://www.soccer-king.jp/worldcupより)

私は、あまりサッカーをみないのですが、ベルギーとフランスが準決勝で戦い、フランスが勝ってフランスが優勝していたようですね。

フランスのランクは7位(2018年6月時点)でしたが、大健闘したようですね。こういったスポーツ競技の結果を予測して、テレビ見て楽しんだら面白そうですね。今度やってみようかな?


いかがだったでしょうか。今回は、FIFAワールドカップの結果を予測し、実際の結果と比べてみました。

最後まで読んでいただきありがとうございました。よろしければこの記事をシェアしていただけると励みになります。よろしくお願いします。

スポンサーリンク
レクタングル広告(大)
レクタングル広告(大)

シェアする

  • このエントリーをはてなブックマークに追加

フォローする

スポンサーリンク
レクタングル広告(大)