Прогнозирование результатов футбольных матчей

от автора

Модель машинного обучения на Python c использованием библиотеки Scikit-learn, для прогнозирования результатов футбольных матчей Российской Премьер Лиги (РПЛ).

Вступление

На написание этой статьи меня вдохновила статья Machine learning: predicting the 2018 EPL mathes. Наша модель машинного обучения будет тренироваться на статистике матчей Российской Премьер Лиги (РПЛ) начиная с сезона 2015/2016, чтобы предсказывать результаты предстоящих игр. Данные взяты с сайта футбольной статистики wyscout.com.
Код и данные доступны в github.

Данные

Подключаем необходимые библиотеки:

import pandas as pd import numpy as np import collections 

Данные с матчами находятся в github.

data = pd.read_csv("RPL.csv", encoding = 'cp1251', delimiter=';') data.head() 

image

Что означает xG и PPDA?

xG (expected goals) – это модель ожидаемых голов. В основе её лежит показатель ударов по воротам, на основе которого мы можем оценить сколько реально голов должна была забить команда, если учесть все удары которые она нанесла. Подробнее о xG.
PPDA (Passes Allowed Per Defensive Action) — футбольный статистический показатель, который позволяет определить интенсивность прессинга в матче. Чем меньше значение PPDA, тем выше интенсивность игры в обороне. Подробнее о PPDA

PPDA = число передач, которое сделала атакующая команда / число действий в обороне

Мы будем прогнозировать результаты матчей для второй части сезона 2018/2019 (т.е. матчи, сыгранные в 2019 году). Список команд играющих в этом сезоне (не учитывая Арсенал, Оренбург, Динамо, Крылья Советов и Енисей, т.к. у них либо отсутствует статистика за прошлые сезоны, либо статистики по ним мало):

RPL_2018_2019 = pd.read_csv('Team Name 2018 2019.csv', encoding = 'cp1251')  teamList = RPL_2018_2019['Team Name'].tolist() teamList 

image

Удаляем матчи с командами, которые не участвуют в сезоне 2018/2019:

deleteTeam = [x for x in pd.unique(data['Команда']) if x not in teamList] for name in deleteTeam:     data = data[data['Команда'] != name]     data = data[data['Соперник'] != name] data = data.reset_index(drop=True) 

Функция, возвращающая статистику команды за сезон:

def GetSeasonTeamStat(team, season):     goalScored = 0 #Голов забито     goalAllowed = 0 #Голов пропущено      gameWin = 0 #Выиграно     gameDraw = 0 #Ничья     gameLost = 0 #Проиграно      totalScore = 0 #Количество набранных очков      matches = 0 #Количество сыгранных матчей          xG = 0 #Ожидаемые голы          shot = 0 #Удары     shotOnTarget = 0 #Удары в створ          cross = 0 #Навесы     accurateCross = 0 #Точные навесы          totalHandle = 0 #Владение мячом     averageHandle = 0 #Среднее владение мячом за матч          Pass = 0 #Пасы     accuratePass = 0 #Точные пасы          PPDA = 0 #Интенсивность прессинга в матче      for i in range(len(data)):         if (((data['Год'][i] == season) and (data['Команда'][i] == team) and (data['Часть'][i] == 2)) or ((data['Год'][i] == season-1) and (data['Команда'][i] == team) and (data['Часть'][i] == 1))):             matches += 1                              goalScored += data['Забито'][i]             goalAllowed += data['Пропущено'][i]              if (data['Забито'][i] > data['Пропущено'][i]):                 totalScore += 3                 gameWin += 1             elif (data['Забито'][i] < data['Пропущено'][i]):                 gameLost +=1             else:                 totalScore += 1                 gameDraw += 1                          xG += data['xG'][i]                          shot += data['Удары'][i]             shotOnTarget += data['Удары в створ'][i]                          Pass += data['Передачи'][i]             accuratePass += data['Точные передачи'][i]                          totalHandle += data['Владение'][i]                          cross += data['Навесы'][i]             accurateCross += data['Точные навесы'][i]                          PPDA += data['PPDA'][i]      averageHandle = round(totalHandle/matches, 3) #Владение мячом в среднем за матч          return [gameWin, gameDraw, gameLost,              goalScored, goalAllowed, totalScore,              round(xG, 3), round(PPDA, 3),             shot, shotOnTarget,              Pass, accuratePass,             cross, accurateCross,             round(averageHandle, 3)] 

Пример использования функции:

GetSeasonTeamStat("Спартак", 2018) #Статистика Спартака за сезон 2017/2018  

image

Для удобства можем дописать код:

returnNames = ["Выиграно", "Ничья", "Проиграно",                "\nГолов забито", "Голов пропущено", "\nНабрано очков",                "\nxG (за сезон)", "PPDA (за сезон)",                "\nУдары", "Удары в створ",                 "\nПасы", "Точные пасы",                "\nНавесы", "Точные навесы",                 "\nВладение (в среднем за матч)"]  for i, n in zip(returnNames, GetSeasonTeamStat("Спартак", 2018)):         print(i, n) 

image

Почему наша статистика отличается от реальной статистики

Реальная статистика Спартака в сезоне 2017/2018:

image

Статистика отличается, т.к. мы учитывали матчи команд которые не играют в РПЛ в сезоне 2018/2019. Т. е., мы не учитываем матчи Спартак — СКА, Спартак — Тосно и тд.

Функция, которая будет возвращать статистику всех команд за сезон:

def GetSeasonAllTeamStat(season):     annual = collections.defaultdict(list)     for team in teamList:         team_vector = GetSeasonTeamStat(team, season)         annual[team] = team_vector     return annual 

Обучение модели

Напишем функцию, которая будет возвращать обучающие данные. Она создает словарь с векторами команд за все сезоны. Для каждой игры функция рассчитывает разницу между векторами команд за определенный сезон и записывает в xTrain. Затем функция присваивает yTrain значение 1, если команда хозяев выигрывает, и 0 в противном случае.

def GetTrainingData(seasons):     totalNumGames = 0     for season in seasons:         annual = data[data['Год'] == season]         totalNumGames += len(annual.index)     numFeatures = len(GetSeasonTeamStat('Зенит', 2016)) #случайная команда для определения размерности     xTrain = np.zeros(( totalNumGames, numFeatures))     yTrain = np.zeros(( totalNumGames ))     indexCounter = 0     for season in seasons:         team_vectors = GetSeasonAllTeamStat(season)         annual = data[data['Год'] == season]         numGamesInYear = len(annual.index)         xTrainAnnual = np.zeros(( numGamesInYear, numFeatures))         yTrainAnnual = np.zeros(( numGamesInYear ))         counter = 0         for index, row in annual.iterrows():             team = row['Команда']             t_vector = team_vectors[team]             rivals = row['Соперник']             r_vector = team_vectors[rivals]                         diff = [a - b for a, b in zip(t_vector, r_vector)]                          if len(diff) != 0:                 xTrainAnnual[counter] = diff             if team == row['Победитель']:                 yTrainAnnual[counter] = 1             else:                  yTrainAnnual[counter] = 0             counter += 1            xTrain[indexCounter:numGamesInYear+indexCounter] = xTrainAnnual         yTrain[indexCounter:numGamesInYear+indexCounter] = yTrainAnnual         indexCounter += numGamesInYear     return xTrain, yTrain 

Поучаем обучающие данные за все сезоны с 2015/2016 по 2018/2019.

years = range(2016,2019) xTrain, yTrain = GetTrainingData(years) 

Для прогнозирования вероятности выигрыша будем использовать алгоритм машинного обучения LinearRegression из библиотеки Scikit-Learn.

from sklearn.linear_model import LinearRegression  model = LinearRegression() model.fit(xTrain, yTrain) 

Напишем функцию, которая будет возвращать прогнозы. Она будет возвращать значение в промежутке от 0 до 1, где 0 — это проигрыш, а 1 — это выигрыш.

def createGamePrediction(team1_vector, team2_vector):     diff = [[a - b for a, b in zip(team1_vector, team2_vector)]]     predictions = model.predict(diff)     return predictions 

Результаты

Для примера посмотрим прогнозы алгоритма на матч Зенит — Спартак

team1_name = "Зенит" team2_name = "Спартак"  team1_vector = GetSeasonTeamStat(team1_name, 2019) team2_vector = GetSeasonTeamStat(team2_name, 2019)  print ('Вероятность, что выиграет ' + team1_name + ':', createGamePrediction(team1_vector, team2_vector)) print ('Вероятность, что выиграет ' + team2_name + ':', createGamePrediction(team2_vector, team1_vector)) 

image

Получается, что в матче Зенит — Спартак вероятность победы Зенита составляет 47% (17.03.2019 Спартак 1-1 Зенит).

Предлагаю делать прогноз учитывая следующее:
До 40% — команда точно не выиграет (проигрыш или ничья)
От 40% до 60% — высокая вероятность ничьи
От 60% — команда точно не проиграет (победа или ничья)

Выведем прогнозы для ЦСКА против всех остальных клубов

for team_name in teamList:     team1_name = "ЦСКА"     team2_name = team_name          if(team1_name != team2_name):         team1_vector = GetSeasonTeamStat(team1_name, 2019)         team2_vector = GetSeasonTeamStat(team2_name, 2019)          print(team1_name, createGamePrediction(team1_vector, team2_vector), " - ", team2_name, createGamePrediction(team2_vector, team1_vector,)) 

image

Алгоритм дал верный прогноз почти на все матчи, которые не закончились в ничью. Единственный неточный прогноз: ЦСКА — Зенит. Вероятность победы ЦСКА выше на 0.001, можно было предположить, что команды равны по силе и сыграют в ничью, но в итоге победил Зенит (3-1).

Вывод

Наш алгоритм является очень примитивным. Он учитывает лишь статистику матчей (и то только 15 основных параметров), а результат в футболе зависит от многих факторов. Даже состояние поля или погода могут повлиять на результат игры.

Дальше хотелось бы увеличить количество признаков, создать тестовую выборку, попробовать различные алгоритмы, настроить модель и получить максимально точные прогнозы.

Буду признателен, если оставите свои идеи и замечания.


ссылка на оригинал статьи https://habr.com/ru/post/456226/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *