Дисклеймер

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

Для проведения исследования использовались: датасет: 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
   <chr>                 <dbl>
 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 по версии машины будет Испания.

Комментарии (2)


  1. Anton888
    11.06.2024 19:50

    В датасете не увидел состав команд, т.е. игроков вообще никак не учитываете?


  1. Kahelman
    11.06.2024 19:50
    +3

    Пока автор свои деньги не поставит - смысла в статье нет.

    Ставить на победителя -глупо. Ставки не вашу пользу. Прелесть чемпионатов Европы/ Мира в том что статистики мало и сеть обучать на ней нельзя. У вас в сети просто все коэффициенты/ на связях не имеют шансов обновиться даже один раз на твой выборке.

    Ставить надо на слабую команду чуть-чуть. Тогда есть шанс выиграть по результатам. Скорее всего будет сеть больше 0, если повезёт. :)

    Сам игрался по такой стратегии на последних чемпионатах. В результате остался при своих с небольшим плюсом