https://habr.com/ru/post/456226/Модель машинного обучения на 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()
Что означает 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
Удаляем матчи с командами, которые не участвуют в сезоне 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
Для удобства можем дописать код:
returnNames = ["Выиграно", "Ничья", "Проиграно",
"\nГолов забито", "Голов пропущено", "\nНабрано очков",
"\nxG (за сезон)", "PPDA (за сезон)",
"\nУдары", "Удары в створ",
"\nПасы", "Точные пасы",
"\nНавесы", "Точные навесы",
"\nВладение (в среднем за матч)"]
for i, n in zip(returnNames, GetSeasonTeamStat("Спартак", 2018)):
print(i, n)
Почему наша статистика отличается от реальной статистикиРеальная статистика Спартака в сезоне 2017/2018:
Статистика отличается, т.к. мы учитывали матчи команд которые не играют в РПЛ в сезоне 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))
Получается, что в матче Зенит — Спартак вероятность победы Зенита составляет 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,))
Алгоритм дал верный прогноз почти на все матчи, которые не закончились в ничью. Единственный неточный прогноз: ЦСКА — Зенит. Вероятность победы ЦСКА выше на 0.001, можно было предположить, что команды равны по силе и сыграют в ничью, но в итоге победил Зенит (3-1).
Вывод
Наш алгоритм является очень примитивным. Он учитывает лишь статистику матчей (и то только 15 основных параметров), а результат в футболе зависит от многих факторов. Даже состояние поля или погода могут повлиять на результат игры.
Дальше хотелось бы увеличить количество признаков, создать тестовую выборку, попробовать различные алгоритмы, настроить модель и получить максимально точные прогнозы.
Буду признателен, если оставите свои идеи и замечания.