Подробное руководство по использованию мультиклассовой классификации для прогнозирования футбольных результатов.
Введение
Я был вдохновлен на создание этой статьи, когда увидел, что Оксфордский университет использовал математические модели для прогнозирования исхода чемпионата мира по футболу 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