Вступление
На написание этой статьи меня вдохновила статья 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()
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)


Статистика отличается, т.к. мы учитывали матчи команд которые не играют в РПЛ в сезоне 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 основных параметров), а результат в футболе зависит от многих факторов. Даже состояние поля или погода могут повлиять на результат игры.
Дальше хотелось бы увеличить количество признаков, создать тестовую выборку, попробовать различные алгоритмы, настроить модель и получить максимально точные прогнозы.
Буду признателен, если оставите свои идеи и замечания.
ссылка на оригинал статьи https://habr.com/ru/post/456226/
Добавить комментарий