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

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

    Вступление


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


    Данные


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

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

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

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

    Средняя зарплата в IT

    110 000 ₽/мес.
    Средняя зарплата по всем IT-специализациям на основании 8 385 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
    Реклама
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее

    Комментарии 20

      +1
      Можно еще взять параметры как: Игра дома или в гостях(дома повышает вероятность судя по статистике), серия из последних матчей(если у команды несколько подряд проигрышей, то вероятность проигрыша увеличивается), необходимо учитывать травмы игроков, в том числе и основных, интенсивность матчей(наверное), в конце сезона турнирное положение(мотивированные команды играют лучше). Можно взять еще не статистические факторы(при увольнении/смене тренера, тоже есть отличие в игре, меняется тактика и т.д.). Можно учитывать громкие трансферы, в какой то степени бюджет клубов для постоянно обучающейся модели. Короче много можно придумать, дерзай) Очень просто и понятно для меня по крайней мере написано
        0
        Главная проблема — нехватка данных (особенно в российском футболе). Компании связанные со спортивной аналитикой предоставляют статистику по матчам, а вот насчёт травм/дисквалификации игроков и т.д. нет данных. Можно смотреть на спортивных сайтах информацию по каждому матчу и таким образом вручную собрать данные по травмам и дисквалификациям.

        А вообще есть такая идея: сравнивать составы команд перед матчем. Например, по суммарной стоимости состава на матч. Но эти данные тоже придется собирать вручную.
          0
          Ну вообще вы с такой идее можете обратится в какую нибудь контору по сбору статистики типа Instat — она российская, может быть их даже ваша инициатива заинтересует(ну или позаимствуют идею как обычно)). Из бесплатных статистических сайтов могу выделить ru.whoscored.com(там много данных, можно наверное даже парсить оттуда), sports.ru и прочие русские аналоги… Стоимость можно брать с transfermarket — достаточно авторитетный сайт.
          Тут наверное не по стоимости можно ориентироваться а по пресловутому xG — оно стабильнее как то
            0
            спарсил, свой xg рассчитал.
              0

              Можно поподробнее? Какой сайт, если не секрет? xG стандартный или своя модель? И если спарсил xG, то и Packing (как xG, только не про удары, а про пасы) можно спарсить

                +1
                sofascore, undestat и некоторые другие.
                xG — своя расчетная формула, которая показала достаточно хорошую точность по сравнению с тем же understat.
                Packing крутой показатель, но пока его не у меня.
                  0

                  Спасибо

        –1
        Теперь можно и савки делать на футбол. Спасибо автору, очень интересная статья.
          +1
          Метрикой качества такого алгоритма может быть суммарный выигрыш в букмекерской конторе, если поставить на все матчи равную сумму в соответствии с его прогнозами.
          Будет стабильно в плюсе — имеет право на жизнь.
            0
            И очень маловероятно, что можно у букмекера таким образом выиграть, потому что у букмекера модель будет точнее
              –1
              В теории букмекер не является игроком, его цель забрать маржу от ставок, а денежные потоки нужно уравновесить. В сущности букмекеру все равно выиграет игрок А или игрок Б. Ему важно, чтобы с учетом всех ставок получить гарантированную прибыль от комиссии. Поэтому в ставках часто наблюдается, так называемое, движение коэффициентов. Когда перекос денежных потоков идет на победу команды X, повышается коэффициент на победу команды Y (для привлечения денежных потоков от ставок на эту команду).
              Развивая данную тему скажу, что если проект задумывается для осуществления ставок, то определение вероятностей исходов недостаточно. Нужно еще и рассчитывать с учетом полученных вероятностей исходов математическое ожидание, исходя из коэффициентов букмекеров. Разумеется, ставить нужно на события с положительным мат.ожиданием. Но основная проблема здесь в достоверном определении вероятностей, это трудно сделать в таком виде спорта как футбол, где исход матчей зависит от субъективных факторов — травмы, удаления, ошибки судей, договорные матчи.
                0
                Вы правы. Скажу больше, основная проблема применения ML к ставкам на спорт — это плохая калибрация предсказанной доходности. Что интересно, классификатор может при это получаться хорошо откалиброванным по вероятности, но это вовсе не обязательно позволяет извлечь прибыль.
              0
              Это да, создавать модель без сравнения её прогнозирующих качеств с таковыми качествами коэффициентов букмейкеров — дело бессмыссленное.
              0
              С русским Вы конечно зря связались, тем более cp1251…
                0
                Огромное спасибо за статью!
                PS некоторые параметры некорректно отобраны. Но это всего лишь мое мнение)
                  0
                  почему во всех результатах прогнозирования для команды А и команды Б сумма составляет 72%?
                    0
                    Интересное замечание, спасибо.

                    Делаем прогноз на победу 1 команды
                    createGamePrediction(team1_vector, team2_vector)

                    Потом делаем прогноз на победу 2 команды
                    createGamePrediction(team2_vector, team1_vector)

                    Получается мы делаем прогнозы на победу/проигрыш, но в футболе есть ничья.
                    Можно добавить такой прогноз
                    team1pred = createGamePrediction(team1_vector, team2_vector)
                    team2pred = createGamePrediction(team2_vector, team1_vector)
                    print("Вероятность победы 1 команды: ", team1pred)
                    print("Вероятность ничьи: ", 1.0-(team1pred+team2pred))
                    print("Вероятность победы 2 команды: ", team2pred)
                    

                    То что ничья получается всегда примерно 28% это странно. Но стоит учитывать, что в РПЛ часто матчи заканчиваются ничьей (примерно 26-32% каждый сезон). Если бы мы обучили модель на матчах Украинской Премьер Лиги (ничья = 22%) или Ла-Лиги Испании (ничья = 22.3%), то получили бы процент ничей на уровне 22-23%. Это дало бы нам сделать более точный прогноз на победу/проигрыш
                      0
                      спасибо за развернутый ответ!
                      я немного доработал эту модель, добавил более 35 показателей за последние 5 лет по РПЛ и у меня общая сумма для команд стала не 72%, а 84%.
                    0
                    Можно ещё анализировать с помощью обработки естественного языка новости перед матчем(пресс-конференции, комментарии и т.д.). Таким образом можно учесть хоть как-то настрой команды на матч, что тоже очень немаловажно в футболе.
                      0
                      сложно, долго и непонятен результат. Главный тренер перед матчем скажет, что скорее всего лучший нападающий не будет играть, т.к. есть микротравма. И? А потом окажется, что он вышел на матч и это была утка.

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое