Подробное руководство по использованию мультиклассовой классификации для прогнозирования футбольных результатов.

Введение
Я был вдохновлен на создание этой статьи, когда увидел, что Оксфордский университет использовал математические модели для прогнозирования исхода чемпионата мира по футболу 2022 года. Будучи заядлым футбольным фанатом и учеником в науке о данных, я решил попробовать. Футбол непредсказуем, часто не идет по пути лучшей команды, поэтому я задумался об использовании данных обо всех международных мужских матчах 2022 года. Могу ли я предсказать вероятного победителя чемпионата мира?
Данные
Я использовал два разных источника данных для этого проекта. Первыми были рейтинги, которые я скопировал с www.eloratings.net. Эти данные предоставляют рейтинги для всех мужских международных футбольных команд и используют рейтинговую систему Эло, изначально предназначенную для рейтинга шахматистов. Я также использовал данные матчей с footystats.org, чтобы собрать все мужские международные игры в 2022 году перед чемпионатом мира.


Подход
Я буду использовать многоклассовые модели классификации для решения этой проблемы, что сильно отличается от подхода, наблюдаемого в примере Оксфордского университета, в котором использовались распределения Пуассона. Я буду использовать этот подход, потому что футбол — это игра с тремя исходами — победа, поражение и ничья — и я заинтересован в применении последних знаний, полученных в университете. Модели классификации с несколькими классами можно обучать с использованием различных алгоритмов, включая деревья решений, случайные леса, методы опорных векторов и нейронные сети. Я протестирую комбинацию этих методов. Тем не менее, независимо от того, какой алгоритм используется, основная идея одна и та же: модель обучается с использованием размеченного набора данных, где каждый пример принадлежит одному из возможных классов. Модель учится делать прогнозы на основе новых данных, находя закономерности в обучающих данных, которые можно использовать для различения разных классов.
Импорт пакетов и определение функций
Чтобы начать проект, я сначала настроил блокнот Jupyter со всеми предварительными условиями, необходимыми для очистки, разработки функций/анализа и моделирования. Это включает
- Полезные пакеты
- Настройка параметров отображения для удобства использования
- Определяя функцию, мне нужно
#Importing Packages
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from scipy.stats import poisson
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.model_selection import cross_val_score, train_test_split, GridSearchCV
from sklearn.metrics import accuracy_score, precision_score, recall_score,f1_score, confusion_matrix, classification_report, multilabel_confusion_matrix
import itertools
import networkx as nx
from networkx.drawing.nx_pydot import graphviz_layout
#Setting Display parameters
pd.set_option('display.max_rows', 1000)
pd.set_option('display.max_columns', 500)
pd.set_option('display.width', 1200)
pd.set_option('display.max_colwidth', 200)
#Defining Functions
#A function to display Confusion Matrix and Classification Reports for Multiclass Classification Algorithms
def Multiclass_Analysis(X_train, y_train, X_test, y_test, model, labels):
y_predict_test = model.predict(X_test)
print("\n")
print('TESTING RESULTS')
print("\n")
print(classification_report(y_test, y_predict_test, target_names=labels, digits=3))
test_cm = multilabel_confusion_matrix(y_test, y_predict_test)
fig, (ax1, ax2, ax3) = plt.subplots(ncols=3,sharey=True, figsize = (7,3))
heatmaps(test_cm, ax1, 0, 'Win')
heatmaps(test_cm, ax2, 1, 'Loss')
heatmaps(test_cm, ax3, 2, 'Draw')
fig.tight_layout()
plt.show()
plt.clf()
y_predict_train = model.predict(X_train)
print("\n")
print('TRAINING RESULTS')
print("\n")
print(classification_report(y_train, y_predict_train, target_names=labels, digits=3))
train_cm = multilabel_confusion_matrix(y_train, y_predict_train)
fig, (ax1, ax2, ax3) = plt.subplots(ncols=3,sharey=True, figsize = (7,3))
heatmaps(train_cm, ax1, 0, 'Win')
heatmaps(train_cm, ax2, 1, 'Loss')
heatmaps(train_cm, ax3, 2, 'Draw')
fig.tight_layout()
plt.show()
#A function to plot Confusion Matrix for Multiclass Classification Algorithms
def heatmaps(cm_list, axis, index, label):
cm = cm_list[index]
cm[0][0] = 0
sns.heatmap(cm, cmap= 'GnBu', annot=True, fmt='g', annot_kws={'size':15}, ax=axis)
axis.set_xlabel('Predicted', fontsize=12)
axis.set_ylabel('Actual', fontsize=12)
axis.set_title(label, fontsize=12)
#A function to return result and points awarded for a football game
def result_finder(home, away):
if home > away:
return pd.Series([0, 3, 0])
if home < away:
return pd.Series([1, 0, 3])
else:
return pd.Series([2, 1, 1])
#A function to position distributions
def find_column_row_number(number_of_values):
col_num = number_of_values
rows = np.floor(np.sqrt(col_num))
while(col_num % rows != 0):
rows = rows - 1
if col_num/rows > 3:
cols = 3
rows = int(np.ceil(col_num/cols))
else:
cols = int(rows)
rows = int(col_num/rows)
if cols > rows:
cols, rows = rows, cols
return cols, rows
#A function to plot distributions
def plot_distribution(df, columns, plot):
col_num = len(columns)
cols, rows = find_column_row_number(col_num)
combinations = list(itertools.product(range(1, rows+1), range(1, cols+1)))[:col_num]
combinations_dict = {columns[i]: combinations[i] for i in range(col_num)}
fig = make_subplots(rows=rows, cols=cols, subplot_titles=(columns))
if plot == 'H':
for c, fig_info in combinations_dict.items():
row_num, col_num = fig_info
fig.add_trace(go.Histogram(x=df[c], name = c, opacity=0.7), row= row_num, col= col_num)
if plot == 'B':
for c, fig_info in combinations_dict.items():
row_num, col_num = fig_info
fig.add_trace(go.Box(x=df[c], name = c, opacity=0.7), row= row_num, col= col_num)
fig.update_yaxes(showticklabels=False)
if plot == 'V':
for c, fig_info in combinations_dict.items():
row_num, col_num = fig_info
fig.add_trace(go.Violin(x=df[c], name = c, box_visible=True, meanline_visible=True, opacity=0.7), row= row_num, col= col_num)
fig.update_yaxes(showticklabels=False)
fig.update_layout(showlegend = False,
autosize=False,
width=1000,
height=1200,
template = 'plotly_white')
fig.show()
#A function to find the best set of parameters
def BestParameters(X, y, model_params):
scores = []
for model_name, mp in model_params.items():
clf = GridSearchCV(mp['model'], mp['params'], cv=3, scoring = 'f1_weighted', return_train_score=True, verbose=2)
clf.fit(X, y)
results = pd.DataFrame(clf.cv_results_)
results = results.sort_values(by='rank_test_score').reset_index(drop=True)
return results
Очистка данных
Затем я перешел к этапу очистки моего проекта, который повлек за собой:
- Импорт данных
- Преобразование данных, чтобы их можно было фильтровать
- Фильтрация данных для правильного периода времени
- Объединение наборов данных
- Создание нескольких ценных функций
- & Расширение фрейма данных, чтобы позволить каждой команде иметь уникальные строки для каждой игры, в которой они принимали участие.
Я расширил фреймворк данных, чтобы убедиться, что могу создавать функции домашней и выездной команды.
#Importing Data
rating = pd.read_csv('international_elo_rating.csv')
fixtures = pd.read_csv('international_recent_fixtures.csv')
#Created Day and Time Columns from "date_GMT"
fixtures[['day','time']] = fixtures['date_GMT'].str.split(" - ",expand=True)
#Made Datetime Column from newly created "day"
fixtures['date'] = pd.to_datetime(fixtures['day'], format='%b %d %Y')
#Filtered data for matches that have taken place in 2022
fixtures = fixtures.loc[(fixtures.status == 'complete')]
fixtures = fixtures.loc[(fixtures.date.dt.to_period('Y') == '2022')].reset_index(drop=True)
#Selected Useful Columns
fixtures = fixtures[['date', 'home_team_name','away_team_name','home_team_goal_count', 'away_team_goal_count', 'team_a_xg', 'team_b_xg', 'home_team_possession', 'away_team_possession']]
#Found elo ratings for Home and Away Teams
team_rating = rating[['team', 'current_rating']]
opposition_rating = rating[['team', 'current_rating']]
team_rating.columns = ['home_team_name', 'home_team_rating']
opposition_rating.columns = ['away_team_name', 'away_team_rating']
#Joined Team Ratings with Fixtures
fixtures = pd.merge(fixtures, team_rating, on='home_team_name', how = 'left')
fixtures = pd.merge(fixtures, opposition_rating, on='away_team_name', how = 'left')
#Dropped Duplicates
fixtures= fixtures.drop_duplicates()
#Used Pre-defined Function to create my target column - Results and assign points to teams
results = fixtures.apply(lambda x: result_finder(x["home_team_goal_count"], x["away_team_goal_count"]), axis=1)
fixtures[["result", "home_team_points", "away_team_points"]] = results
#Expanded Dataframe so each fixture was broken up into 2 rows on for the away to and one for the home
home_team = fixtures[['date','home_team_name','home_team_goal_count', 'away_team_goal_count', 'team_a_xg', 'team_b_xg', 'home_team_possession', 'away_team_possession', 'home_team_rating', 'away_team_rating', 'result', 'home_team_points']]
away_team = fixtures[['date','away_team_name','home_team_goal_count', 'away_team_goal_count', 'team_a_xg', 'team_b_xg', 'away_team_possession', 'home_team_possession', 'away_team_rating', 'home_team_rating', 'result', 'away_team_points']]
home_team.columns = ['date','team_name', 'goals_scored', 'goals_conceeded', 'xg', 'xga', 'possession', 'opponents_possession', 'rating', 'opponents_rating', 'result', 'team_points']
away_team.columns = ['date','team_name', 'goals_conceeded', 'goals_scored', 'xga', 'xg', 'possession', 'opponents_possession', 'rating', 'opponents_rating', 'result', 'team_points']
home_team = home_team[['date','team_name', 'goals_scored', 'goals_conceeded', 'xg', 'xga', 'possession', 'opponents_possession', 'rating', 'opponents_rating', 'result', 'team_points']]
away_team = away_team[['date','team_name', 'goals_scored', 'goals_conceeded', 'xg', 'xga', 'possession', 'opponents_possession', 'rating', 'opponents_rating', 'result', 'team_points']]
team_info= pd.concat([home_team, away_team]).sort_values("date").reset_index(drop=True)
Разработка функций
Затем я перешел к элементу Feature Engineering моего проекта, где я сделал большую часть своих функций. Это включает:
- Среднее ожидаемое количество голов — avg_xg — количество голов, которое команда ожидает забить в среднем в зависимости от качества использованных моментов.
- Среднее ожидаемое количество голов против — avg_xga — количество голов, которое команда ожидает пропустить в среднем, исходя из качества имеющихся моментов.
- Average Possession For — avg_poss_for — среднее количество владения мячом командой.
- Average Possession For — avg_poss_agn — среднее количество владения мячом командой соперника.
- Average Goals For — avg_goals_for — среднее количество голов, забитых командой.
- Average Goals Against — avg_goals_agn — среднее количество голов, которое команда пропускает.
- Средний рейтинг — avg_rating — средний рейтинг Эло команды.
- Средний рейтинг оппонентов — avg_opponents_rating — средний рейтинг Эло оппонентов, с которыми они столкнулись.
- Средние баллы — avg_points — средний балл, начисленный команде.
Затем я сделал функции, производные от первоначальных функций, которые показали различия этих функций.
Я завершил этап разработки признаков, отбросив недостающие значения и создав окончательный фрейм данных, который будет использоваться после этапа анализа признаков для создания модели.
#Feature Creation
#Creating Aggregate Features for Each Team
agg_features = team_info.groupby('team_name', as_index=False).agg(avg_xg=pd.NamedAgg(column="xg", aggfunc="mean"),
avg_xga=pd.NamedAgg(column="xga", aggfunc="mean"),
avg_poss_for=pd.NamedAgg(column="possession", aggfunc="mean"),
avg_poss_agn=pd.NamedAgg(column="opponents_possession", aggfunc="mean"),
avg_goals_for=pd.NamedAgg(column="goals_scored", aggfunc="mean"),
avg_goals_agn=pd.NamedAgg(column="goals_conceeded", aggfunc="mean"),
avg_rating=pd.NamedAgg(column="rating", aggfunc="mean"),
avg_opponents_rating=pd.NamedAgg(column="opponents_rating", aggfunc="mean"),
avg_points=pd.NamedAgg(column="team_points", aggfunc="mean"))
#Creating Avg Differences between teams and their opposition
agg_features['avg_dif_xg'] = agg_features['avg_xg'] - agg_features['avg_xga']
agg_features['avg_dif_poss'] = agg_features['avg_poss_for'] - agg_features['avg_poss_agn']
agg_features['avg_dif_goals'] = agg_features['avg_goals_for'] - agg_features['avg_goals_agn']
agg_features['avg_dif_rating'] = agg_features['avg_rating'] - agg_features['avg_opponents_rating']
#Creating all features Home and Away
home_agg_features = agg_features.copy()
away_agg_features = agg_features.copy()
home_agg_features.columns = ['home_'+str(col) for col in home_agg_features.columns]
away_agg_features.columns = ['away_'+str(col) for col in away_agg_features.columns]
#Merging Features with Games
df = fixtures[['date','home_team_name','away_team_name', 'result']]
df = pd.merge(df, home_agg_features, on='home_team_name', how = 'inner')
df = pd.merge(df, away_agg_features, on='away_team_name', how = 'inner')
#Creating Dataframe with inversed fixtures to remove any home or away bias
df_inversed = fixtures[['date','away_team_name', 'home_team_name', 'result']]
df_inversed.columns = ['date','home_team_name','away_team_name', 'result']
df_inversed = df_inversed.replace(0, 3)
df_inversed = df_inversed.replace(1, 4)
df_inversed = df_inversed.replace(4, 0)
df_inversed = df_inversed.replace(3, 1)
df_inversed = pd.merge(df_inversed, home_agg_features, on='home_team_name', how = 'inner')
df_inversed = pd.merge(df_inversed, away_agg_features, on='away_team_name', how = 'inner')
#Creating Final Dataframe
final_df = pd.concat([df, df_inversed]).sort_values("date").reset_index(drop=True)
#Removing matches where XG and XGA is not recorded
final_df = final_df.loc[(final_df.home_avg_xg != 0) & (final_df.away_avg_xg != 0)]
#Finding Missing Values
missing_count = final_df.isnull().sum()
missing_df = pd.DataFrame({'column':missing_count.index, 'missing_count':missing_count.values})
missing_df = missing_df.sort_values(by= ['missing_count'], ascending=False).reset_index(drop=True)
display(missing_df)
#Dropping Missing Values
final_df = final_df.dropna()
Анализ и выбор функций
Чтобы начать анализ признаков, я искал мультиколлинеарность и отбрасывал коллинеарные признаки, используя тепловую карту.
#Making Features Dataframe
features = final_df.iloc[:, 4:]
#Setting size for Correlation Matrix
plt.figure(figsize=(12,12))
#Creating Correlation Data
corr_matrix = features.corr()
#Plotting Correlation Matrix
sns.heatmap(
corr_matrix,
annot=True,
annot_kws={"fontsize": 10},
linewidths=0.5,
center=0.00,
fmt=".2f",
xticklabels=corr_matrix.columns,
yticklabels=corr_matrix.columns,
cmap = sns.diverging_palette(10, 240, n=9),
vmin=-1,
vmax=1)
plt.show()

Затем я посмотрел на распределения функций, которые я создал. Я гарантировал, что все они были относительно нормально распределены, так как это одно из предположений для большинства моделей машинного обучения.
Dropping Collinear Features features = features.drop(['home_avg_poss_for','home_avg_poss_agn', 'home_avg_dif_goals', 'away_avg_dif_goals', 'away_avg_points', 'away_avg_poss_for'], axis=1) #Plotting Distributions of Features plot_distribution(features,features.columns, 'H')

Моделирование, настройка гиперпараметров и оценка
Затем я создал сетку параметров, которую использовал для хранения переменных гиперпараметров и подходов к классификации, которые хотел протестировать. Затем я использовал это в качестве входных данных для GridSearchCV, который использует перекрестную проверку и итерацию по заданной сетке параметров, чтобы найти лучшие гиперпараметры на основе метрики оценки. В этом примере я использовал взвешенную оценку F1. Предопределенная функция «BestParameters» использовалась для вывода лучших результатов. Затем я нашел набор параметров, которые возвращали высокие баллы как за поезд, так и за тест, и использовал эти параметры для моей окончательной модели.
#Creating the models and hypertuning parameters
model_params = {
'decision_tree': {
'model': DecisionTreeClassifier(random_state=42),
'params': {
'criterion': ['gini','entropy'],
'min_samples_split' : list(range(2,10)),
'min_samples_leaf' : list(range(2,10)),}},
'random_forrest': {
'model': RandomForestClassifier(random_state=42),
'params': {
"n_estimators" : [100, 300, 500, 800, 1200],
"max_depth" : [5, 8, 15, 25, 30],
"min_samples_split" : [2, 5, 10, 15, 100],
"min_samples_leaf" : [1, 2, 5, 10] ,
"max_features": ["sqrt"]}},
'gradient_boosting': {
'model': GradientBoostingClassifier(random_state=42),
'params': {
"learning_rate": [0.01, 0.1, 0.5],
"min_samples_split": [5, 10],
"min_samples_leaf": [3, 5],
"max_depth":[3,5,10],
"max_features":["sqrt"],
"n_estimators":[100, 200]}}
#Making Features and Labels
X = features
y = final_df.result
#Using Param Grid defined to find the best model
clf_df = BestParameters(X, y, model_params)
#Displaying the top 10 best models
display(clf_df.head(10)[['rank_test_score', 'params', 'mean_test_score', 'mean_train_score']])

#Creating Train and Test Data X_train, X_test, y_train, y_test = train_test_split(X, y, test_size= 0.2, random_state=1) #Fitting Model with Best Parameters gb = GradientBoostingClassifier(random_state=42, learning_rate = 0.01, max_depth = 3, max_features = 'sqrt', min_samples_leaf = 3, min_samples_split = 10, n_estimators = 200) gb.fit(X_train.values, np.ravel(y_train)) #Creating Confusion Matrix labels = ['Win','Loss','Draw'] Multiclass_Analysis(X_train, y_train, X_test, y_test, gb, labels) #Retraining Model with all the Data gb = GradientBoostingClassifier(random_state=42, learning_rate = 0.01, max_depth = 3, max_features = 'sqrt', min_samples_leaf = 3, min_samples_split = 10, n_estimators = 200) gb.fit(X.values, np.ravel(y)
Затем я проанализировал показатели прогнозирования выбранной мной модели с помощью отчета о классификации и матрицы путаницы, созданной моей предопределенной функцией. Поскольку я предсказываю мультиклассовую классификацию, я удалил предсказание истинных отрицательных результатов для ясности визуализации.


Прогнозирование результатов игры
Поскольку я нашел модель, которую буду использовать, я мог начать самое интересное. Время предсказывать чемпионат мира! В ходе многих итераций своих блокнотов я создал показанную ниже функцию, которая прогнозирует и выводит результаты в зависимости от играющих команд и стадии соревнования.
def knockout_stage_simulator(fixtures, stage=None, tracking_list=None):
for i, fixture in enumerate(sorted(fixtures)):
game = fixture[0]
team_1 = fixture[1]
team_2 = fixture[2]
team_1_home_features = world_cup[team_1]['HomeFeatures']
team_1_away_features = world_cup[team_1]['AwayFeatures']
team_2_home_features = world_cup[team_2]['HomeFeatures']
team_2_away_features = world_cup[team_2]['AwayFeatures']
normal = [team_1_home_features + team_2_away_features]
inversed = [team_2_home_features + team_1_away_features]
prob_1 = gb.predict_proba(normal)
prob_2 = gb.predict_proba(inversed)
win_percentage = (prob_1[0][0] + prob_2[0][1])/2
loss_percentage = (prob_1[0][1] + prob_2[0][0])/2
draw_percentage = (prob_1[0][2] + prob_2[0][2])/2
prob_list = [win_percentage, loss_percentage, draw_percentage]
result = prob_list.index(max(prob_list))
if stage == 'GS':
world_cup[team_1]['AverageWinPercentage'] += win_percentage/3
world_cup[team_2]['AverageWinPercentage'] += loss_percentage/3
if result == 0:
print(f"Group {game} - {team_1} vs {team_2} : {team_1} Win with a Probability of {round(win_percentage,2)}")
world_cup[team_1]['GroupPoints'] +=3
elif result == 1:
print(f"Group {game} - {team_1} vs {team_2} : {team_2} Win with a Probability of {round(loss_percentage,2)}")
world_cup[team_2]['GroupPoints'] +=3
else:
print(f"Group {game} - {team_1} vs {team_2} : The Game Results in a Draw with a Probability of {round(draw_percentage,2)}")
world_cup[team_1]['GroupPoints'] +=1
world_cup[team_2]['GroupPoints'] +=1
if (i+1) % 6 == 0:
sorted_groups = sorted({team : [world_cup[team]["GroupPoints"], world_cup[team]["AverageWinPercentage"]] for team in world_cup_group_stages[game]}.items(), key=lambda p:p[1], reverse=True)
winner = sorted_groups[0][0]
second = sorted_groups[1][0]
group_winners_list.append(winner)
group_second_list.append(second)
print("\n")
print(f"Group {group} Standings :")
for k,v in sorted_groups:
print(f"{k} -- {v[0]}")
print("\n")
if stage in ['R16', 'Q', 'S']:
result_list = [team_1, team_2, round(win_percentage, 2), round(loss_percentage,2)]
knockout_games.append(result_list)
if result == 2:
min_prob = prob_list.index(min(prob_list))
if min_prob == 0:
result = 1
else:
result = 0
if result == 0:
print(f"Game {game} - {team_1} vs {team_2} : {team_1} Win with a Probability of {round(win_percentage,2)}")
tracking_list.append(team_1)
if stage == 'S':
tracking_list[0].append(team_1)
tracking_list[1].append(team_2)
else:
print(f"Game {game} - {team_1} vs {team_2} : {team_2} Win with a Probability of {round(loss_percentage,2)}")
tracking_list.append(team_2)
if stage == 'S':
tracking_list[0].append(team_2)
tracking_list[1].append(team_1)
if stage == 'F':
if game == 1:
result_list = [team_1, team_2, round(win_percentage, 2), round(loss_percentage,2)]
knockout_games.append(result_list)
if result == 0:
print(f"World Cup Final - {team_1} vs {team_2} : {team_1} Win the World Cup Final! with a Probability of {round(win_percentage,2)}")
else:
print(f"World Cup Final - {team_1} vs {team_2} : {team_2} Win the World Cup Final! with a Probability of {round(loss_percentage,2)}")
else:
if result == 0:
print(f"World Cup Third Place - {team_1} vs {team_2} : {team_1} Win the World Cup Third Place! with a Probability of {round(win_percentage,2)}")
else:
print(f"World Cup Third Place - {team_1} vs {team_2} : {team_2} Win the World Cup Third Place! with a Probability of {round(loss_percentage,2)}")
Создание хранилища данных
Я создал различные структуры данных, включая списки и вложенные словари, для хранения команд, групп, фикстур и прогнозов, выводимых моделью.
#A list of teams in the world cup
world_cup_teams = ["Qatar", "Ecuador", "Senegal", "Netherlands", "England", "Iran", "United States", "Wales", "Argentina", "Saudi Arabia", "Mexico", "Poland", "France", "Australia", "Denmark", "Tunisia", "Spain", "Costa Rica", "Germany", "Japan", "Belgium", "Canada", "Morocco", "Croatia", "Brazil", "Serbia", "Switzerland", "Cameroon", "Portugal", "Ghana", "Uruguay", "South Korea"]
#A list of the teams corresponding groups
world_cup_groups = ["A", "A", "A", "A","B", "B", "B", "B","C", "C", "C", "C", "D", "D", "D", "D", "E", "E", "E", "E", "F", "F", "F", "F", "G", "G", "G", "G", "H", "H", "H", "H"]
#Creating a dictionary to hold team data
world_cup = {team: {"Group" : group, "GroupPoints" : 0, "AverageWinPercentage": 0, "HomeFeatures" : list(home_features.loc[(home_features.home_team_name == team)].values[0][1:]), "AwayFeatures" : list(away_features.loc[(away_features.away_team_name == team)].values[0][1:])} for team, group in zip(world_cup_teams, world_cup_groups)}
world_cup_group_stages = {"A":[], "B":[], "C":[], "D":[], "E":[], "F":[], "G":[], "H":[]}
world_cup_group_fixtures = []
#Creating a dictionary to hold data about which group each team is assigned to
for team, group in zip(world_cup_teams, world_cup_groups):
world_cup_group_stages[group].append(team)
#Creating a the fixtures for the group stages
for k in world_cup_group_stages.keys():
print()
group_k_games = list(itertools.combinations(world_cup_group_stages[k], 2))
for j in range(len(group_k_games)):
world_cup_group_fixtures.append([k] + list(group_k_games[j]))
#Creating list to store results data
group_winners_list = []
group_second_list = []
round_16_winners_list = []
quater_final_winners_list = []
semi_final_winners_list = []
semi_final_second_list = []
knockout_games = []
#Creating list to store fixture data
world_cup_round_16_fixtures = []
world_cup_quater_final_fixtures = []
world_cup_semi_final_fixtures = []
world_cup_final_fixtures = []
Моделирование матчей
Затем я использовал функцию прогнозирования в тандеме с циклами for, создающими приспособления, для моделирования чемпионата мира от группового этапа до финала. Результат этого показан в нижней части учебника.
#Simulating Group Stages
print("Group Stages \n")
knockout_stage_simulator(world_cup_group_fixtures, stage='GS', tracking_list=[group_winners_list, group_second_list])
#Creating Fixtures for Round of 16
for i in range(8):
if i % 2 == 0:
world_cup_round_16_fixtures.append([int((i/2) +1), group_winners_list[i], group_second_list[i+1]])
else:
world_cup_round_16_fixtures.append([int(((i-1)/2) + 5), group_winners_list[i], group_second_list[i-1]])
#Simulating Round of 16
print("\n Round of 16 \n")
knockout_stage_simulator(world_cup_round_16_fixtures, stage='R16', tracking_list=round_16_winners_list)
#Creating Fixtures for Quarter Finals
for i in range(0, 8, 2):
world_cup_quater_final_fixtures.append([int((i/2)+1),round_16_winners_list[i], round_16_winners_list[i+1]])
#Simulating Quarter Finals
print("\n Quarter Finals \n")
knockout_stage_simulator(world_cup_quater_final_fixtures, stage='Q', tracking_list=quater_final_winners_list)
#Creating Fixtures for Semi Finals
for i in range(0, 4, 2):
world_cup_semi_final_fixtures.append([int((i/2)+1),quater_final_winners_list[i], quater_final_winners_list[i+1]])
#Simulating Semi Finals
print("\n Semi Finals \n")
knockout_stage_simulator(world_cup_semi_final_fixtures, stage='S', tracking_list=[semi_final_winners_list, semi_final_second_list])
#Creating Fixtures for Finals
for i in range(2):
if i % 2 == 0:
world_cup_final_fixtures.append([1 , semi_final_winners_list[0], semi_final_winners_list[1]])
else:
world_cup_final_fixtures.append([0 , semi_final_second_list[0], semi_final_second_list[1]])
#Simulating Quarter Finals
print('\n Finals \n')
knockout_stage_simulator(world_cup_final_fixtures, stage='F')
Создание визуализации
Наконец, я создал визуализацию плей-офф соревнований. Я черпал вдохновение у Серхио Пессоа, чтобы создать этот красивый визуальный ряд.
plt.figure(figsize=(15, 11))
G = nx.balanced_tree(2, 3)
labels = []
list_rev = list(reversed(knockout_games))
for game in list_rev:
label = f"{game[0]} ({game[2]}) \n{game[1]} ({game[3]})"
labels.append(label)
labels_dict = {i:label for i, label in enumerate(labels)}
pos = {0: (226.56, 217.56), 1: (226.56, 289.56), 2: (226.56, 145.56), 3: (328.38, 319.38), 4: (124.73, 319.38), 5: (124.73, 115.73), 6: (328.38, 115.73), 7: (426.12, 300.22), 8: (309.22, 417.12), 9: (143.9, 417.12), 10: (27.0, 300.22), 11: (27.0, 134.9), 12: (143.9, 18.0), 13: (309.22, 18.0), 14: (426.12, 134.9)}
labels_pos = {n: (k[0], k[1]-0.08*k[1]) for n,k in pos.items()}
center = pd.DataFrame(pos).mean(axis=1).mean()
nx.draw(G, pos = pos, with_labels=False, node_color=range(15), edge_color="#d1f4ff", width=10, font_weight='bold',cmap=plt.cm.Blues_r, node_size=5000)
nx.draw_networkx_labels(G, pos = labels_pos, bbox=dict(boxstyle="round,pad=0.3", fc="white", ec="black", lw=.5, alpha=1),
labels=labels_dict)
plt.axis('equal')
plt.show()
Нокаутировать

Заключение
Отвечая на вопрос, который я задал в начале этой статьи, я не уверен в предсказаниях этой модели, поскольку несколько команд, которые вряд ли пройдут групповые этапы, поздно выходят в плей-офф. Однако фаворитом на победу в чемпионате мира является Бразилия, что кажется разумным, а Аргентина выходит в полуфинал, что весьма вероятно. Ожидается, что другая сторона жеребьевки будет неточной: Иран, Катар, Коста-Рика и Уругвай выйдут на этапы конкурса, что будет шоком для большинства. Эти необычные результаты, вероятно, связаны с сильной формой этих более слабых команд, участвующих в турнире. В целом, эту модель можно было бы улучшить, используя больше данных от команд и игроков за более длительный период, потому что короткое окно матчей не позволяло модели предсказывать результаты с большой уверенностью.
Заключительные замечания
Надеюсь, вам понравилось следовать этому уроку. Пожалуйста, найдите мой репозиторий GitHub, если вы хотите попробовать это сами.
Надеюсь, вам понравится чемпионат мира!
Group Stages Group A - Ecuador vs Netherlands : The Game Results in a Draw with a Probability of 0.51 Group A - Ecuador vs Senegal : The Game Results in a Draw with a Probability of 0.71 Group A - Qatar vs Ecuador : The Game Results in a Draw with a Probability of 0.63 Group A - Qatar vs Netherlands : Netherlands Win with a Probability of 0.85 Group A - Qatar vs Senegal : Qatar Win with a Probability of 0.44 Group A - Senegal vs Netherlands : Netherlands Win with a Probability of 0.9 Group A Standings : Netherlands -- 7 Qatar -- 4 Ecuador -- 3 Senegal -- 1 Group B - England vs Iran : Iran Win with a Probability of 0.5 Group B - England vs United States : The Game Results in a Draw with a Probability of 0.36 Group B - England vs Wales : England Win with a Probability of 0.62 Group B - Iran vs United States : The Game Results in a Draw with a Probability of 0.38 Group B - Iran vs Wales : Iran Win with a Probability of 0.74 Group B - United States vs Wales : United States Win with a Probability of 0.56 Group B Standings : Iran -- 7 United States -- 5 England -- 4 Wales -- 0 Group C - Argentina vs Mexico : Argentina Win with a Probability of 0.86 Group C - Argentina vs Poland : Argentina Win with a Probability of 0.76 Group C - Argentina vs Saudi Arabia : Argentina Win with a Probability of 0.83 Group C - Mexico vs Poland : The Game Results in a Draw with a Probability of 0.39 Group C - Saudi Arabia vs Mexico : The Game Results in a Draw with a Probability of 0.56 Group C - Saudi Arabia vs Poland : Poland Win with a Probability of 0.44 Group C Standings : Argentina -- 9 Poland -- 4 Mexico -- 2 Saudi Arabia -- 1 Group D - Australia vs Denmark : Denmark Win with a Probability of 0.65 Group D - Australia vs Tunisia : Australia Win with a Probability of 0.48 Group D - Denmark vs Tunisia : Denmark Win with a Probability of 0.85 Group D - France vs Australia : France Win with a Probability of 0.51 Group D - France vs Denmark : Denmark Win with a Probability of 0.76 Group D - France vs Tunisia : France Win with a Probability of 0.69 Group D Standings : Denmark -- 9 France -- 6 Australia -- 3 Tunisia -- 0 Group E - Costa Rica vs Germany : The Game Results in a Draw with a Probability of 0.46 Group E - Costa Rica vs Japan : Costa Rica Win with a Probability of 0.47 Group E - Germany vs Japan : The Game Results in a Draw with a Probability of 0.58 Group E - Spain vs Costa Rica : Spain Win with a Probability of 0.43 Group E - Spain vs Germany : The Game Results in a Draw with a Probability of 0.67 Group E - Spain vs Japan : Spain Win with a Probability of 0.6 Group E Standings : Spain -- 7 Costa Rica -- 4 Germany -- 3 Japan -- 1 Group F - Belgium vs Canada : Belgium Win with a Probability of 0.45 Group F - Belgium vs Croatia : Croatia Win with a Probability of 0.62 Group F - Belgium vs Morocco : Belgium Win with a Probability of 0.54 Group F - Canada vs Croatia : Croatia Win with a Probability of 0.68 Group F - Canada vs Morocco : Canada Win with a Probability of 0.58 Group F - Morocco vs Croatia : Croatia Win with a Probability of 0.66 Group F Standings : Croatia -- 9 Belgium -- 6 Canada -- 3 Morocco -- 0 Group G - Brazil vs Cameroon : Brazil Win with a Probability of 0.96 Group G - Brazil vs Serbia : Brazil Win with a Probability of 0.85 Group G - Brazil vs Switzerland : Brazil Win with a Probability of 0.85 Group G - Serbia vs Cameroon : Serbia Win with a Probability of 0.88 Group G - Serbia vs Switzerland : Serbia Win with a Probability of 0.71 Group G - Switzerland vs Cameroon : Switzerland Win with a Probability of 0.45 Group G Standings : Brazil -- 9 Serbia -- 6 Switzerland -- 3 Cameroon -- 0 Group H - Ghana vs South Korea : South Korea Win with a Probability of 0.88 Group H - Ghana vs Uruguay : Uruguay Win with a Probability of 0.91 Group H - Portugal vs Ghana : Portugal Win with a Probability of 0.86 Group H - Portugal vs South Korea : Portugal Win with a Probability of 0.59 Group H - Portugal vs Uruguay : Uruguay Win with a Probability of 0.61 Group H - Uruguay vs South Korea : Uruguay Win with a Probability of 0.76 Group H Standings : Uruguay -- 9 Portugal -- 6 South Korea -- 3 Ghana -- 0 Round of 16 Game 1 - Netherlands vs United States : Netherlands Win with a Probability of 0.8 Game 2 - Argentina vs France : Argentina Win with a Probability of 0.72 Game 3 - Spain vs Belgium : Spain Win with a Probability of 0.63 Game 4 - Brazil vs Portugal : Brazil Win with a Probability of 0.74 Game 5 - Iran vs Qatar : Iran Win with a Probability of 0.38 Game 6 - Denmark vs Poland : Denmark Win with a Probability of 0.78 Game 7 - Croatia vs Costa Rica : Costa Rica Win with a Probability of 0.35 Game 8 - Uruguay vs Serbia : Uruguay Win with a Probability of 0.68 Quarter Finals Game 1 - Netherlands vs Argentina : Argentina Win with a Probability of 0.39 Game 2 - Spain vs Brazil : Brazil Win with a Probability of 0.54 Game 3 - Iran vs Denmark : Denmark Win with a Probability of 0.56 Game 4 - Costa Rica vs Uruguay : Uruguay Win with a Probability of 0.53 Semi Finals Game 1 - Argentina vs Brazil : Brazil Win with a Probability of 0.55 Game 2 - Denmark vs Uruguay : Uruguay Win with a Probability of 0.64 Finals World Cup Third Place - Argentina vs Denmark : Argentina Win the World Cup Third Place! with a Probability of 0.69 World Cup Final - Brazil vs Uruguay : Brazil Win the World Cup Final! with a Probability of 0.7