Предсказываем результаты группового этапа и победителя Евро 2024 при помощи машинного обучения и чата GPT 4.0

daf73ead4089aebbe6bef5a480e3b382.jpg

Дисклеймер

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

Для проведения исследования использовались: датасет: https://www.kaggle.com/datasets/martj42/international-football-results-from-1872-to-2017? resource=download, язык программирования R, чат GPT 4.0. 

Цели исследования: 1) проверить точность прогноза в результате машинного обучения на основании базы данных за 20 лет 2) узнать размер выигрыша/проигрыша в букмекерской конторе при использовании приведенного подхода.

Проблематика исследования: здесь не учтены важные факторы такие как уровень и стоимость игроков, текущая форма команд, фактор домашнего турнира для сборной Германии и многое другое.

Это все учтено самими букмекерами при выставлении коэффициентов на матчи, минус 10–15 процентов их маржи, поэтому просто выбирая фаворитов выиграть невозможно.

Лично для меня больший интерес представляет ответ на вопрос — удастся ли машине обнаружить неочевидные закономерности и обыграть букмекера, а не определение фаворита.

Методология

В первую очередь был обработан датасет, так как он включает результаты более 47 000 матчей за 152 года, в том числе — различных африканских квалификаций, которые нам не интересны и замедлили бы обработку данных, датасет был сокращен до результатов евро, квалификации к нему и лиге наций.

*квалификацию к миру брать не стал, т.к. хотя команды те же, это другой турнир и формат немного отличается*

За точку отсчёта взят евро 1996 года и как следствие квалификация к нему, начиная с 1994 года. Такое решение связано с изменением формата турнира, а также с развалом стран социалистического блока (увеличилось количество стран участниц).

Таким образом мы получаем примерно один состав участников и результаты за последние 20 лет. Финальный датасет составил 2 758 матчей.

Далее с помощью чата GPT я перебрал несколько вариантов машинного обучения в Python (использовал: pandas, numpy, train_test_split, GridSearchCV, RandomForestClassifier, accuracy_score).

Лучшим результатом стала точность прогноза — 53.51%.

Точность прогноза получилось улучшить, используя язык R.

Лучшим результатом на R стала точность прогноза — 57.65%

Весьма неплохой процент, учитывая, что игра идет на 3 результата. Так как процент точности на R у нас выше, будем использовать его для прогнозирования.

> library(randomForest)
> library(dplyr)
> 
> # Загрузка данных
> data <- read.csv("filtered_results.csv")
> 
> # Преобразование столбца date в формат даты
> data$date <- as.Date(data$date, format="%Y-%m-%d")
> 
> # Создание целевой переменной
> data$result <- ifelse(data$home_score > data$away_score, 1, 
+                       ifelse(data$home_score < data$away_score, -1, 0))
> 
> # Преобразование данных в единый формат
> home_games <- data %>%
+     select(team = home_team, opponent = away_team, score = home_score, opponent_score = away_score, result)
> 
> away_games <- data %>%
+     select(team = away_team, opponent = home_team, score = away_score, opponent_score = home_score, result) %>%
+     mutate(result = ifelse(result == 1, -1, ifelse(result == -1, 1, 0)))
> 
> all_games <- bind_rows(home_games, away_games)
> 
> # Создание новых признаков
> team_stats <- all_games %>%
+     group_by(team) %>%
+     summarise(total_games = n(),
+               total_win_rate = mean(result == 1),
+               total_avg_score = mean(score))
> 
> # Подготовка данных для модели
> data <- data %>%
+     left_join(team_stats, by = c("home_team" = "team")) %>%
+     rename(home_team_total_games = total_games,
+            home_team_total_win_rate = total_win_rate,
+            home_team_total_avg_score = total_avg_score) %>%
+     left_join(team_stats, by = c("away_team" = "team")) %>%
+     rename(away_team_total_games = total_games,
+            away_team_total_win_rate = total_win_rate,
+            away_team_total_avg_score = total_avg_score)
> 
> # Проверка и замена NA значений
> data[is.na(data)] <- 0
> 
> # Подготовка данных для модели
> features <- c("home_team_total_win_rate", "away_team_total_win_rate", 
+               "home_team_total_games", "away_team_total_games", 
+               "home_team_total_avg_score", "away_team_total_avg_score")
> X <- data[features]
> y <- factor(data$result)
> 
> # Разделение данных на обучающую и тестовую выборки
> set.seed(42)
> train_indices <- sample(seq_len(nrow(data)), size = 0.8 * nrow(data))
> X_train <- X[train_indices, ]
> y_train <- y[train_indices]
> X_test <- X[-train_indices, ]
> y_test <- y[-train_indices]
> 
> # Обучение модели Random Forest
> rf_model <- randomForest(X_train, y_train, ntree=200, mtry=3, importance=TRUE)
> 
> # Предсказание на тестовой выборке
> y_pred <- predict(rf_model, X_test)
> accuracy <- sum(y_pred == y_test) / length(y_test)
> print(paste("Accuracy:", accuracy))
[1] "Accuracy: 0.576576576576577"
> 
> # Пример новых матчей
> new_matches <- data.frame(
+     home_team = c("Germany", "Hungary", "Spain", "Italy", "Poland", "Slovenia", "Serbia", "Romania", "Belgium", "Austria", 
+                   "Turkey", "Portugal", "Croatia", "Germany", "Scotland", "Slovenia", "Denmark", "Spain", "Slovakia", 
+                   "Poland", "Netherlands", "Georgia", "Turkey", "Belgium", "Switzerland", "Scotland", "Albania", "Croatia", 
+                   "Netherlands", "France", "England", "Denmark", "Slovakia", "Ukraine", "Georgia", "Czech Republic"),
+     away_team = c("Scotland", "Switzerland", "Croatia", "Albania", "Netherlands", "Denmark", "England", "Ukraine", "Slovakia", 
+                   "France", "Georgia", "Czech Republic", "Albania", "Hungary", "Switzerland", "Serbia", "England", "Italy", 
+                   "Ukraine", "Austria", "France", "Czech Republic", "Portugal", "Romania", "Germany", "Hungary", "Spain", 
+                   "Italy", "Austria", "Poland", "Slovenia", "Serbia", "Romania", "Belgium", "Portugal", "Turkey")
+ )
> 
> # Расчет признаков для новых матчей
> new_matches <- new_matches %>%
+     left_join(team_stats, by = c("home_team" = "team")) %>%
+     rename(home_team_total_win_rate = total_win_rate,
+            home_team_total_games = total_games,
+            home_team_total_avg_score = total_avg_score) %>%
+     left_join(team_stats, by = c("away_team" = "team")) %>%
+     rename(away_team_total_win_rate = total_win_rate,
+            away_team_total_games = total_games,
+            away_team_total_avg_score = total_avg_score)
> 
> # Проверка и замена NA значений
> new_matches[is.na(new_matches)] <- 0
> 
> # Предсказание результатов новых матчей
> predictions <- predict(rf_model, new_matches[features])
> results <- ifelse(predictions == 1, "Home Win", ifelse(predictions == 0, "Draw", "Away Win"))
> 
> # Вывод результатов
> for (i in 1:nrow(new_matches)) {
+     print(paste(new_matches$home_team[i], "vs", new_matches$away_team[i], "-> Prediction:", results[i]))

Результаты группового этапа:

1. Germany vs Scotland → Prediction: Home Win

2. Hungary vs Switzerland → Prediction: Home Win

3. Spain vs Croatia → Prediction: Home Win

4. Italy vs Albania → Prediction: Home Win

5. Poland vs Netherlands → Prediction: Away Win

6. Slovenia vs Denmark → Prediction: Draw

7. Serbia vs England → Prediction: Draw

8. Romania vs Ukraine → Prediction: Home Win

9. Belgium vs Slovakia → Prediction: Home Win

10. Austria vs France → Prediction: Away Win

11. Turkey vs Georgia → Prediction: Home Win

12. Portugal vs Czech Republic → Prediction: Home Win

13. Croatia vs Albania → Prediction: Home Win

14. Germany vs Hungary → Prediction: Home Win

15. Scotland vs Switzerland → Prediction: Home Win

16. Slovenia vs Serbia → Prediction: Home Win

17. Denmark vs England → Prediction: Draw

18. Spain vs Italy → Prediction: Home Win

19. Slovakia vs Ukraine → Prediction: Home Win

20. Poland vs Austria → Prediction: Home Win

21. Netherlands vs France → Prediction: Away Win

22. Georgia vs Czech Republic → Prediction: Away Win

23. Turkey vs Portugal → Prediction: Away Win

24. Belgium vs Romania → Prediction: Draw

25. Switzerland vs Germany → Prediction: Away Win

26. Scotland vs Hungary → Prediction: Home Win

27. Albania vs Spain → Prediction: Away Win

28. Croatia vs Italy → Prediction: Draw

29. Netherlands vs Austria → Prediction: Home Win

30. France vs Poland → Prediction: Home Win

31. England vs Slovenia → Prediction: Home Win

32. Denmark vs Serbia → Prediction: Home Win

33. Slovakia vs Romania → Prediction: Away Win

34. Ukraine vs Belgium → Prediction: Home Win

35. Georgia vs Portugal → Prediction: Away Win

36. Czech Republic vs Turkey → Prediction: Home Win

Посмотрим, каким образом сформировалась сетка 1/8 плей-офф с учетом полученных результатов матчей.

library(randomForest)
> library(dplyr)
> 
> # Загрузка данных
> data <- read.csv("filtered_results.csv")
> 
> # Преобразование столбца date в формат даты
> data$date <- as.Date(data$date, format="%Y-%m-%d")
> 
> # Создание целевой переменной
> data$result <- ifelse(data$home_score > data$away_score, 1, 
+                       ifelse(data$home_score < data$away_score, -1, 0))
> 
> # Преобразование данных в единый формат
> home_games <- data %>%
+     select(team = home_team, opponent = away_team, score = home_score, opponent_score = away_score, result)
> 
> away_games <- data %>%
+     select(team = away_team, opponent = home_team, score = away_score, opponent_score = home_score, result) %>%
+     mutate(result = ifelse(result == 1, -1, ifelse(result == -1, 1, 0)))
> 
> all_games <- bind_rows(home_games, away_games)
> 
> # Создание новых признаков
> team_stats <- all_games %>%
+     group_by(team) %>%
+     summarise(total_games = n(),
+               total_win_rate = mean(result == 1),
+               total_avg_score = mean(score))
> 
> # Подготовка данных для модели
> data <- data %>%
+     left_join(team_stats, by = c("home_team" = "team")) %>%
+     rename(home_team_total_games = total_games,
+            home_team_total_win_rate = total_win_rate,
+            home_team_total_avg_score = total_avg_score) %>%
+     left_join(team_stats, by = c("away_team" = "team")) %>%
+     rename(away_team_total_games = total_games,
+            away_team_total_win_rate = total_win_rate,
+            away_team_total_avg_score = total_avg_score)
> 
> # Проверка и замена NA значений
> data[is.na(data)] <- 0
> 
> # Подготовка данных для модели
> features <- c("home_team_total_win_rate", "away_team_total_win_rate", 
+               "home_team_total_games", "away_team_total_games", 
+               "home_team_total_avg_score", "away_team_total_avg_score")
> X <- data[features]
> y <- factor(data$result)
> 
> # Разделение данных на обучающую и тестовую выборки
> set.seed(42)
> train_indices <- sample(seq_len(nrow(data)), size = 0.8 * nrow(data))
> X_train <- X[train_indices, ]
> y_train <- y[train_indices]
> X_test <- X[-train_indices, ]
> y_test <- y[-train_indices]
> 
> # Обучение модели Random Forest
> rf_model <- randomForest(X_train, y_train, ntree=200, mtry=3, importance=TRUE)
> 
> # Предсказание на тестовой выборке
> y_pred <- predict(rf_model, X_test)
> accuracy <- sum(y_pred == y_test) / length(y_test)
> print(paste("Accuracy:", accuracy))
[1] "Accuracy: 0.576576576576577"
> 
> # Групповой этап
> group_stage_matches <- data.frame(
+     home_team = c("Germany", "Hungary", "Spain", "Italy", "Poland", "Slovenia", "Serbia", "Romania", "Belgium", "Austria", 
+                   "Turkey", "Portugal", "Croatia", "Germany", "Scotland", "Slovenia", "Denmark", "Spain", "Slovakia", 
+                   "Poland", "Netherlands", "Georgia", "Turkey", "Belgium", "Switzerland", "Scotland", "Albania", "Croatia", 
+                   "Netherlands", "France", "England", "Denmark", "Slovakia", "Ukraine", "Georgia", "Czech Republic"),
+     away_team = c("Scotland", "Switzerland", "Croatia", "Albania", "Netherlands", "Denmark", "England", "Ukraine", "Slovakia", 
+                   "France", "Georgia", "Czech Republic", "Albania", "Hungary", "Switzerland", "Serbia", "England", "Italy", 
+                   "Ukraine", "Austria", "France", "Czech Republic", "Portugal", "Romania", "Germany", "Hungary", "Spain", 
+                   "Italy", "Austria", "Poland", "Slovenia", "Serbia", "Romania", "Belgium", "Portugal", "Turkey")
+ )
> 
> # Расчет признаков для группового этапа
> group_stage_matches <- group_stage_matches %>%
+     left_join(team_stats, by = c("home_team" = "team")) %>%
+     rename(home_team_total_win_rate = total_win_rate,
+            home_team_total_games = total_games,
+            home_team_total_avg_score = total_avg_score) %>%
+     left_join(team_stats, by = c("away_team" = "team")) %>%
+     rename(away_team_total_win_rate = total_win_rate,
+            away_team_total_games = total_games,
+            away_team_total_avg_score = total_avg_score)
> 
> # Проверка и замена NA значений
> group_stage_matches[is.na(group_stage_matches)] <- 0
> 
> # Предсказание результатов группового этапа
> predictions <- predict(rf_model, group_stage_matches[features])
> results <- ifelse(predictions == 1, "Home Win", ifelse(predictions == 0, "Draw", "Away Win"))
> 
> # Вывод результатов и подсчет очков
> group_stage_matches <- group_stage_matches %>%
+     mutate(result = results,
+            home_points = ifelse(result == "Home Win", 3, ifelse(result == "Draw", 1, 0)),
+            away_points = ifelse(result == "Away Win", 3, ifelse(result == "Draw", 1, 0)))
> 
> # Создание таблицы очков
> group_points <- group_stage_matches %>%
+     select(home_team, home_points) %>%
+     rename(team = home_team, points = home_points) %>%
+     bind_rows(group_stage_matches %>%
+                   select(away_team, away_points) %>%
+                   rename(team = away_team, points = away_points)) %>%
+     group_by(team) %>%
+     summarise(total_points = sum(points)) %>%
+     arrange(desc(total_points))
> 
> # Вывод очков команд
> print(group_points)
# A tibble: 24 × 2
   team           total_points
                    
 1 France                    9
 2 Germany                   9
 3 Portugal                  9
 4 Spain                     9
 5 Romania                   7
 6 Czech Republic            6
 7 Netherlands               6
 8 Scotland                  6
 9 Denmark                   5
10 England                   5
# ℹ 14 more rows
# ℹ Use `print(n = ...)` to see more rows
> 
> # Определение команд, вышедших в плей-офф
> groups <- list(
+     A = c("Germany", "Scotland", "Hungary", "Switzerland"),
+     B = c("Spain", "Croatia", "Italy", "Albania"),
+     C = c("Slovenia", "Denmark", "Serbia", "England"),
+     D = c("Poland", "Netherlands", "Austria", "France"),
+     E = c("Belgium", "Slovakia", "Romania", "Ukraine"),
+     F = c("Turkey", "Georgia", "Portugal", "Czech Republic")
+ )
> 
> playoff_teams <- list()
> third_place_teams <- list()
> 
> for (group in names(groups)) {
+     group_teams <- groups[[group]]
+     group_points_filtered <- group_points %>% filter(team %in% group_teams)
+     playoff_teams[[group]] <- group_points_filtered$team[1:2]
+     third_place_teams[[group]] <- group_points_filtered$team[3]
+ }
> 
> # Определение лучших третьих мест
> third_place_teams_points <- group_points %>% filter(team %in% unlist(third_place_teams))
> best_third_place_teams <- third_place_teams_points %>% arrange(desc(total_points)) %>% head(4) %>% pull(team)
> 
> # Заполнение расписания матчей плей-офф
> playoff_schedule <- data.frame(
+     match = c("Match № 38", "Match № 37", "Match № 40", "Match № 39", "Match № 42", "Match № 41", "Match № 43", "Match № 44"),
+     home_team = c(playoff_teams$A[2], playoff_teams$A[1], playoff_teams$C[1], playoff_teams$B[1], playoff_teams$D[2], playoff_teams$F[1], playoff_teams$E[1], playoff_teams$D[1]),
+     away_team = c(playoff_teams$B[2], playoff_teams$C[2], best_third_place_teams[1], best_third_place_teams[2], playoff_teams$E[2], best_third_place_teams[3], best_third_place_teams[4], playoff_teams$F[2])
+ )
> 
> print(playoff_schedule)

1/8 плей-офф:

Match № 38

Scotland

Croatia

Match № 37

Germany

England

Match № 40

Denmark

Italy

Match № 39

Spain

Slovenia

Match № 42

Netherlands

Belgium

Match № 41

Portugal

Hungary

Match № 43

Romania

Poland

Match № 44

France

Czech Republic

На этом завершается первый этап исследования.

На втором этапе я подведу промежуточные итоги и дам прогноз на плей-офф с учетом реально образовавшихся пар в 1/8.

На третьем этапе подведу общие итоги.

Оценка результатов исследования:

1) Посмотрим, сколько результатов было предсказано верно и сравним процент с 57.65. Так проверим, насколько верно компьютер оценил точность своего прогноза.

2) Посмотрим виртуальный банк после турнира и проверим, удалось ли машине обыграть букмекера.

Виртуальный банк

Для того чтобы узнать, принесет нам прибыль или убыток в букмекерской конторе такая стратегия, мы создадим виртуальный банк в размере 5 300 долларов. 51 матч будет сыгран на этом турнире, на каждый будет совершена условная ставка в размере 100 долларов на основании прогноза машины + 2 раза по 100 долларов мы поставим на чемпиона — до начала турнира и после окончания групповой стадии.

Я буду брать средний коэффициент на сайте https://www.flashscore.com.ua/, чтобы не рекламировать какого-то конкретного букмекера.

А чемпионом Евро 2024 по версии машины будет Испания.

© Habrahabr.ru