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