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

Модель машинного обучения на 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 основных параметров), а результат в футболе зависит от многих факторов. Даже состояние поля или погода могут повлиять на результат игры.

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

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

© Habrahabr.ru