С наближаването на Световното първенство по футбол 2022 интересът и дискусиите за това кой отбор ще спечели първенството нарастват. Затова реших да тествам уменията си за наука за данни и машинно обучение, за да разработя модел, анализиращ исторически данни, за да симулирам всички игри от Световното първенство по футбол 2022.

Източник на данни

За да изградим модел на машинно обучение, се нуждаем от данните от екипите. Първо, имаме нужда от нещо, което разказва представянето на отборите, което може да бъде извлечено от предишни игри. Освен това реших да използвам класацията на FIFA при изграждането на характеристиките. Те ще бъдат полезни за количествено определяне на качеството на противника, с който отборът се е изправил в даден мач. И двете данни могат да бъдат намерени в Kaggle: минали игри и Класиране на FIFA.

Изграждане на набор от данни

Кои характеристики могат да повлияят при определянето на победителя във футболен мач? Този въпрос има много открит отговор. От избраните играчи до температурата на стадиона през деня, всичко може да окаже влияние върху резултата. След това избрах да изградя набор от данни само с миналите статистики от всеки отбор, участващ в играта, като приоритизирам количествено измеримите статистики, които могат да бъдат събрани по прост начин, като например поставени голове, средно класиране, спечелени точки и други, които ще бъдете подробни. Тези данни могат да бъдат намерени в съединяването на двата набора от данни, за които говорих в последния раздел.

Освен това ще бъде анализирано само представянето в цикъла на Световното първенство през 2022 г. Идеята е да се вземе предвид само вариацията на представянето при подготовката за Световното първенство.

import pandas as pd
import re
df =  pd.read_csv("games/results.csv") #games between national teams
df["date"] = pd.to_datetime(df["date"])
df = df[(df["date"] >= "2018-8-1")].reset_index(drop=True) #games at the 2022 wc cycle
df_wc = df #pre-wc outcomes

rank = pd.read_csv("fifa_ranking-2022-10-06.csv") #rankings
rank["rank_date"] = pd.to_datetime(rank["rank_date"]) 
rank = rank[(rank["rank_date"] >= "2018-8-1")].reset_index(drop=True) #selecting games from the 2022 wc cycle
rank["country_full"] = rank["country_full"].str.replace("IR Iran", "Iran").str.replace("Korea Republic", "South Korea").str.replace("USA", "United States") #ajustando nomes de algumas seleções
rank = rank.set_index(['rank_date']).groupby(['country_full'], group_keys=False).resample('D').first().fillna(method='ffill').reset_index()
rank_wc = rank #dataframe with rankings

#Making the merge
df_wc_ranked = df_wc.merge(rank[["country_full", "total_points", "previous_points", "rank", "rank_change", "rank_date"]], left_on=["date", "home_team"], right_on=["rank_date", "country_full"]).drop(["rank_date", "country_full"], axis=1)
df_wc_ranked = df_wc_ranked.merge(rank[["country_full", "total_points", "previous_points", "rank", "rank_change", "rank_date"]], left_on=["date", "away_team"], right_on=["rank_date", "country_full"], suffixes=("_home", "_away")).drop(["rank_date", "country_full"], axis=1)

По-горе е кодът за създаване на желания набор от данни.

Наборът от данни е готов, както можете да видите по-горе, и има повече информация за гостуващия отбор. Важно е да запомните, че по-голямата част от мачовете между националните отбори се играят на неутрално място, но ще използвам имената „отбор домакин“ и „отбор гост“, за да опростя начина, по който ще говоря за участващите отбори.

Разработка на функции

Сега имам нужда от набор от кандидат функции. С тях мога да направя анализ, който ще покаже дали функцията има предсказваща сила или не, за да разбера дали трябва да запазя, премахна или модифицирам функцията. Характеристиките, които изчислих като кандидати, бяха:

  • Среден брой голове в цикъла на Световната купа и в последните 5 мача.
  • Средно отбелязани голове в цикъла на Световната купа и в последните 5 мача.
  • Разлика в позициите в ранглистата на FIFA между всеки отбор.
  • Класиране на FIFA, с което всеки отбор се е сблъсквал средно в мачовете от цикъла на Световното първенство и в последните 5 мача.
  • Увеличение на точките в ранглистата на FIFA от първия мач от цикъла и сега.
  • Увеличение на точките в класацията на FIFA преди 5 мача и сега.
  • Средно спечелени точки от играта в цикъла на Световната купа и в последните 5 игри.
  • Средно спечелените точки от играта, претеглени според позицията в класирането в цикъла на Световната купа и в последните 5 игри.
  • Категорична променлива, показваща дали играта е била приятелска или не.

Първите две характеристики се използват за количествено определяне на нападателната сила и защитната сила на отбора. Разликата между позицията в ранглистата на FIFA в мача се използва за количествено определяне на разликата между силните страни на двата отбора, изчислена от FIFA. Средното класиране се използва, за да се анализират силните страни на опонентите, с които се е изправил един отбор.

Увеличението на точките в ранглистата на FIFA беше изчислено, за да се анализира повишаването на качеството на отбора по време на цикъла на Световната купа и в последните 5 мача.

Средната стойност на игровите точки, спечелени от отбора, служи чисто за количествено определяне на представянето на отбора, докато претеглената средна стойност на игровите точки, спечелени от отбор, се претегля от позицията на противника в класирането, с която отборът се е изправил, за да се анализира дали представянето на отбора се дължи на високо на противници от ниско ниво.

За изграждане на функции въз основа на класирането на FIFA има два избора: използване на точки за класиране на FIFA или използване на позиция в класирането на FIFA. Избрах да използвам позицията в класирането във всички функции, с изключение на увеличението в класирането. Не създадох същата функция, променяйки само позицията в класирането на FIFA по точки за класиране на FIFA, защото тези колони са много отрицателно свързани, както можете да видите по-долу.

След това няма смисъл да се създават две версии на функции като среднопретеглените точки в играта според класацията, като се използват точките в класацията и позициите в класацията, тъй като те ще дадат един и същ резултат.

Анализ на данни

Преди моделирането е необходимо да се анализира какво ще се прогнозира. Въпреки това идеалното е да се предвиди между победа, равенство и загуба, проблемът с класификацията в 3 класа е много труден за анализ и оценка. Тогава реших да направя прогнозата между два класа: победа на домакинския отбор и равенство/загуба на домакинския отбор.

За да анализирам характеристиките във връзка с целта, ще използвам диаграми на цигулка и графики в кутия. Идеята е да се анализира как техните разпределения се държат по отношение на стойностите на всеки клас и дали могат да разделят добре данните.

За първия набор от данни вече създаденият, само rank_dif, разликата между класирането на двата отбора, оказва влияние върху целевите класове. По този начин ще създам повече функции въз основа на разликите, тъй като изглежда, че те са добри предсказатели:

  • Голова разлика.
  • Претърпени голове разлика.
  • Разлика между голове, направени от отбор и претърпени от противника.

И отново анализирах сюжетите за цигулка.

Характеристиките на разликата между цели и понесени цели оказват добро въздействие. Въпреки това функциите, които картографират разликите между голове, направени от отбор и понесени от противника, нямат влияние. Следователно сега имаме:

  • Разлика в класирането.
  • Голова разлика от цикъла на световното първенство и в последните 5 мача.
  • Претърпяна голова разлика в цикъла на световното първенство и в последните 5 мача.

Също така можем да направим разликата в точките, разликата в позицията в класирането и разликата в точките, спечелени от срещата в класирането. И, за да претегля нивото на противника, се сетих за следната характеристика: разлика между отбелязаните голове според срещата в класирането и една и съща за претърпените голове.

Изгледът от цигулка на тези данни не е много ефективен поради малкия мащаб. След това ще анализирам и боксплота на тези данни.

Разликата в точките, разликата в головете според класирането, разликата в точките според класирането и разликата в класирането са добри характеристики. Но тяхната версия за последните 5 игри и тяхната версия на пълния цикъл изглежда корелират. Ще проверя това по-долу.

Анализирайки корелацията, се вижда, че при разликата в целите, направени чрез класиране, идеалът е да се избере само една от версиите, а аз избрах тази, която отчита пълния цикъл. За останалите е възможно да използвате и двете версии.

След това имаме като функции:

  • Разлика между класирането (rank_dif)
  • Разлика между средния брой голове в цикъла на Световната купа и в последните 5 мача (goals_dif/goals_dif_l5)
  • Разлика между средния брой отбелязани голове в цикъла на Световната купа и в последните 5 мача (goals_suf_dif/goals_suf_dif_l5)
  • Разлика между средната позиция в класирането в цикъла на Световната купа и в последните 5 игри (dif_rank_agst/dif_rank_agst_l5)
  • Разлика между целите, претеглени от средната стойност на играча в класирането в цикъла на Световната купа (goals_per_ranking_dif)
  • Разлика между точките, претеглени от средната стойност на играча в класирането в цикъла на Световната купа и в последните 5 мача (dif_points_rank/dif_points_rank_l5)
  • Категорична променлива, която показва дали е приятелски или не (is_friendly)

С това имаме база данни с необходимите функции за прилагане на модела на машинно обучение.

Модел

Идеята ми тук е да създам два модела, един на Random Forest и друг на Gradient Boosting, и да ги сравня, за да намеря кой е по-добър, за да се използва в симулацията. Реших да използвам модели, базирани на дървета на решенията, защото те се справяха по-добре с футболни проблеми, когато изучавах литературата. Освен това не виждам необходимост от използване на по-сложни модели поради размера на набора от данни.

Ще направя вариант на параметър с помощта на „GridSearchCV“ на SkLearn и ще използвам най-добрия модел в симулацията.

from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.model_selection import train_test_split, GridSearchCV

#separating the target from the features
X = model_db.iloc[:, 3:]
y = model_db[["target"]]

#dividing the database
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size= 0.2, random_state=1)
gb = GradientBoostingClassifier(random_state=5)
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]
         } 
gb_cv = GridSearchCV(gb, params, cv = 3, n_jobs = -1, verbose = False)
gb_cv.fit(X_train.values, np.ravel(y_train))

#getting the best model
gb = gb_cv.best_estimator_

Избягвах да тествам много параметри поради забавянето на изпълнението, приоритизирах тестове със стойности, които намаляват overfit, като rate_in_rate не толкова нисък и n_estimators не толкова висок.

Направи същото с Случайната гора:

params_rf = {"max_depth": [20],
                "min_samples_split": [5, 10],
                "max_leaf_nodes": [175, 200],
                "min_samples_leaf": [5, 10],
                "n_estimators": [250],
                 "max_features": ["sqrt"],
                }

rf = RandomForestClassifier(random_state=1)
rf_cv = GridSearchCV(rf, params_rf, cv = 3, n_jobs = -1, verbose = False)
rf_cv.fit(X_train.values, np.ravel(y_train))

Анализирах моделите с матрица на объркване и ROC крива и резултатите бяха:

analyze(gb)

analyze(rf)

Моделът Random Forest има малко по-добро представяне, но изглежда прекалява. Анализирайки AUC-ROC на Gradient Boosting, виждаме модел с почти същата производителност, но с по-нисък риск от претоварване, затова беше избран.

Симулация на световно първенство

Сега стигнахме до най-интересната част: вижте кой отбор ще прогнозира моделът да спечели Световната купа!

Първото нещо, което трябва да направите, е да получите списък с отбори, които играят Световно първенство, и използвах метода read_html от Pandas. Методът взема рамка от данни от уеб страница, в която поставям страницата на Световното първенство Wikipedia. С това ще пресъздам таблицата за Световната купа.

Таблицата съдържа игрите, точките на всеки отбор в групата и списък, съхраняващ вероятностите отборът да спечели всяка игра. Това ще се използва като тайбрек в случай на два отбора с еднакви точки в групата.

Както вече обясних, моделът прави класификацията между победа от отбора домакин и победа/равенство от отбора гост. Тогава как можем да предвидим равенствата? Създадох обективно правило за това: знаейки, че всички мачове от Световното първенство се играят на неутрално място, прогнозата ще бъде направена в две форми:

  • Отбор A x Отбор B (Симулация 1)
  • Отбор B x Отбор A (Симулация 2)

Ако отбор А или отбор Б спечели и двете прогнози, тогава победата се приписва на този отбор. В случай, че единият отбор спечели при първата прогноза, а другият отбор спечели при втората прогноза, тогава ще се определи равенство. Във фазата на плейофите вероятностите ще бъдат изчислени и в двете прогнози и отборът с най-висока средна вероятност напредва. Тъй като моделът приписва „победа“ на отбора гост, дори ако завърши наравно, вероятността за равенство е в рамките на вероятността за отбора гост. Така че с този тип симулация и двата отбора получават симулация с равенство като предимство.

Данните, използвани в симулацията, са до последния мач на отбора. С други думи, за Бразилия характеристиките ще бъдат изчислени до мача срещу Тунис, който беше последният, в който Бразилия игра.

Сега можем да стартираме код, симулиращ игра по игра, изчислявайки точките и виждайки какво се случва в първата фаза.

Резултатът беше:

Интересно е да се видят някои резултати, като равенството между Бразилия и Швейцария и Дания и Франция. Като цяло фаворитите преминаха груповата фаза.

В плейофите идеята е същата:

За да видя резултатите тук, освен изходния текст, реших да начертая графика с изображението на плейофа, както беше направено в този бележник на Kaggle. Това е много интересен начин да видите резултатите от тези проблеми.

Световната купа беше симулирана! Моят модел прогнозира победа от Бразилия, с 56% вероятност срещу Англия на финала! Мисля, че най-голямото разочарование беше преминаването на Белгия срещу Германия и достигането на Англия до финала, елиминирайки Франция на четвъртфиналите. Интересно е да се видят някои игри, в които вероятността е много малка, като Холандия срещу Аржентина. От четвъртфиналите до финала нито един отбор не продължи напред с повече от 60% вероятност, което показва, че по-голямата част от отборите, които преминаха към плейофите, са на подобно ниво.

Заключение

Идеята на този проект беше да практикувам знанията си в машинното обучение, използвайки нещо, което харесвам, футбол. Мислех, че симулацията на Световната купа е много интересна, тъй като е актуална тема в днешно време, която привлича вниманието на всеки, който харесва спорта. Вярвам, че целта беше постигната, тъй като изграждането на всички функции и анализът на данните ми даде възможност да търся и да се запозная с много нови техники.

Що се отнася до резултатите, много се надявам, че моделът ще предскаже правилния шампион!

В случай, че можете да видите кода в детайли, можете да видите моя GitHub и моя Kaggle. И ако искате да се свържете с мен, това е моят LinkedIn. Благодаря!

Идентифицирате ли се като Latinx и работите в областта на изкуствения интелект или познавате ли някой, който е Latinx и работи в областта на изкуствения интелект?

Не забравяйте да натиснете 👏 по-долу, за да подпомогнете нашата общност — това означава много!

Благодаря :)