Я начал этот проект в начале этой недели, используя сценарий из статьи об ожидаемых целях, которую я опубликовал ранее в этом месяце. Я понял, что understat.com на самом деле моделирует вероятность выигрыша-ничьи-выигрыша для матчей, которые они предоставляют под капотом, то есть эти прогнозы не видны во внешнем интерфейсе. На фоне этого я подумал, что было бы здорово обучить модель, которая в значительной степени учится на этих данных и стремится воспроизвести это, чтобы пользователь мог войти и предсказать матч за пределами своего покрытия лиги…. и так родилось приложение для прогнозирования результатов матчей xG.

Посмотреть развернутое приложение можно здесь — https://leftsidecentrehalf-xg-match-sim-app-qylmi1.streamlit.app/

со всем кодом на моем github repo.

Теперь перейдем к этапам извлечения данных, моделирования и развертывания.

Библиотеки

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

import re
import json
import requests
import pandas as pd
import warnings
from sklearn.preprocessing import StandardScaler
from sklearn.tree import DecisionTreeRegressor
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import r2_score,mean_squared_error,make_scorer
from xgboost import XGBRegressor
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import cross_val_score
import numpy as np
from sklearn.model_selection import GridSearchCV, KFold
import pickle

Извлечение данных

Как упоминалось во вступлении, у меня уже был сценарий, созданный на основе другой статьи, в которой были собраны данные по определенной лиге за определенный сезон. Understat предоставил данные почти за десять лет для 6 лиг. Названия команд или лиг не важны, но, введя как можно больше значений, я смог создать набор данных почти из 18 000 строк, который должен быть приличного размера для модели, на которой можно учиться. Несколько простых изменений в скрипте, который я использовал в предыдущей статье, позволили мне довольно легко собрать все эти данные. Если вы читали предыдущую статью, то единственными изменениями являются два списка и использование вложенного цикла for в переменной URL-адреса строки f.

warnings.filterwarnings('ignore')

seasons = [2014,2015,2016,2017,2018,2019,2020,2021,2022]
competitions = ['EPL','La_liga','Serie_A','Bundesliga','Ligue_1','RFPL']

all_data = []
for season in seasons:
    for comp in competitions:
        url = f"https://understat.com/league/{comp}/{season}"
        html_doc = requests.get(url).text

        data = re.search(r"datesData\s*=\s*JSON\.parse\('(.*?)'\)", html_doc).group(1)
        data = re.sub(r'\\x([\dA-F]{2})', lambda g: chr(int(g.group(1), 16)), data)
        data = json.loads(data)

        for d in data:
            all_data.append({
                'season': season,
                'competition': comp,
                'date': d['datetime'][:10], # first ten letters
                'home_team': d['h']['title'],
                'away_team': d['a']['title'],
                'home_goals': d["goals"]["h"],
                'away_goals': d["goals"]["a"],
                'home_xG':d['xG']['h'],
                'away_xG': d['xG']['a'],
                'forecast': list(d.get('forecast', {}).values())
            })

df = pd.DataFrame(all_data)
# Split the forecast list into separate columns
df[['home_win_prob', 'draw_prob', 'away_win_prob']] = df['forecast'].apply(lambda x: pd.Series(x))

# Drop the original forecast column
df = df.drop('forecast', axis=1)

# Drop the games that haven't been played
df = df.dropna(how='any', subset=None)

df.to_csv('xg_model.csv',index=False)

Приведенный выше скрипт на момент написания возвращает следующие столбцы и строки без необходимости очистки. Для целей модели меня интересовали только столбцы home_xG,away_xG и вероятности совпадения.

Моделирование

Поскольку целевыми переменными являются home_win_prob, draw_prob иaway_win_prob, это многомерная проблема. Это не проблема классификации, а проблема регрессии, поскольку я пытаюсь предсказать эти значения на основе пользовательских данных. Я попробовал как можно больше моделей, чтобы получить максимальное значение r2, среднеквадратичную ошибку и среднеквадратичную ошибку, так как это наиболее важные функции стоимости для увеличения (в случае r- в квадрате, который учитывает, сколько дисперсии учитывает модель) и уменьшают mse и rmse (чем ниже значение RMSE или MSE, тем лучше производительность регрессионной модели, поскольку это означает, что прогнозируемые значения ближе к фактическим значениям)

Предварительная обработка

Я использовал стандартное масштабирование перед моделированием, как это принято в науке о данных, поскольку прогностические модели обычно работают лучше, когда входные переменные имеют аналогичный масштаб, однако на самом деле это не так, и потому что я не использую нейронную сеть. решил продолжить без масштабирования в конце). Другой шаг предварительной обработки, который я предпринял, заключался в том, чтобы перетасовать данные в наборы для обучения — тестирования и проверки 60% — 20% и 20%. Перетасовывая данные, я намеревался получить как можно больше уникальных оценок xG, чтобы модель могла предсказывать широкий диапазон значений.

Результаты модели

Таблицы 1 и 2 показывают результаты 4 моделей, использованных для этой задачи. Это модели DecisionTreeRegressor, LinearRegression, XGBoostRegressor и RandomForrestRegressor. Я предоставляю результаты для обучающих и тестовых наборов. Изучив оба набора результатов, я смог оценить потенциальную проблему переобучения-недообучения моделей. Можно заметить, что регрессор XGBoost работает лучше всего с точки зрения самого высокого значения r2 и самых низких значений mse и rmse. В некоторых случаях, таких как линейная регрессия, модель выглядит недостаточно приспособленной, т. е. она работает лучше на тестовом наборе, чем на обучающем. Просто отметим, что XG означает экстремальный градиент — тип подхода к оптимизации модели, а не ожидаемых целей — но здесь это очень удобно.

Итак, теперь, когда я определил лучшую модель (в наборе, который я выбрал — там могут быть лучшие модели!) Я решил оптимизировать регрессор XGBoost с помощью подхода, называемого настройкой гиперпараметров. Следует рассмотреть два основных подхода — поиск по сетке и случайный поиск. Поиск по сетке включает указание сетки возможных значений гиперпараметров, а затем оценку производительности модели для каждой комбинации гиперпараметров в сетке. Производительность обычно измеряется с использованием метода перекрестной проверки, например перекрестной проверки k-кратного размера. Grid Search исчерпывающе просматривает все возможные комбинации гиперпараметров и возвращает комбинацию, обеспечивающую наилучшую производительность.

Случайный поиск, с другой стороны, включает в себя случайную выборку гиперпараметров из указанного распределения, а затем оценку производительности модели для каждого набора гиперпараметров. Этот метод часто используется, когда гиперпараметры имеют непрерывное или большое пространство поиска, что делает непрактичным исчерпывающий поиск по всем возможным комбинациям.

Поскольку параметры, которые я использовал в XGBoost Regressor, дали мне достойный результат — я выбрал поиск по сетке, чтобы попытаться получить результат в пределах известного пространства параметров. Во время этого процесса я также использовал K-кратную перекрестную проверку, которая позволяет нам более надежно оценить производительность модели, чем мы могли бы получить, используя одно разделение поезд-тест, потому что мы используем все доступные данные для обоих. обучение и аттестация. Кроме того, перекрестная проверка в k-кратном порядке может помочь выявить переоснащение, поскольку мы проверяем модель на данных, которые она не видела во время обучения. Я использовал результаты этой настройки гиперпараметров в качестве руководства для дальнейшей настройки параметров модели.

Ниже приведены параметры последней модели, которую я использовал. В таблицах 3 и 4 показаны новые оценки на тренировочном и тестовом наборах.

# Train an XGBoost Regressor model
xgb_model = XGBRegressor(n_estimators=100, learning_rate=0.1, max_depth=10,colsample_bytree=1,
                         min_child_weight=1,subsample=1,gamma=0.5, random_state=42)

На этом этапе я могу использовать проверочный набор, показанный в таблице 5, который модель никогда не видела во время обучения, чтобы еще раз увидеть, насколько велики или велики ошибки при прогнозировании новых данных. Значения немного хуже, чем в тренировочном наборе, и в значительной степени имитируют то, что наблюдалось в тестовом наборе. Качественный товар.

Разовое предсказание

Прежде чем я решил сохранить модель и создать приложение, я решил проверить на практике, как модель работает при прогнозировании вероятностей одной игры. По сути, я надеюсь, что здесь модель максимально приблизится к заниженным значениям. Я выбрал недавнюю игру между «Арсеналом» и «Пэлас», где «Арсенал» выиграл со счетом 4–1. В соответствии с заниженной статистикой в ​​файле данных вероятность победы в матче xG оценивается как 0,438,0,2882,0,2738.

Существует требование, чтобы сумма предсказанных вероятностей равнялась 1, что я и делаю, нормализуя их. Модель предсказывает 0,4620, 0,2780 и 0,2600, которые не совсем совпадают, но довольно близки и достаточно хороши, чтобы я мог уверенно двигаться дальше!

Сохранение модели

Теперь я могу сохранить окончательный регрессор XGBoost локально для развертывания. Библиотека pickle допускает это, что означает, что я могу загрузить это без необходимости обучения и т. д.

# Save the trained model to a file
with open('xgb_model.pkl', 'wb') as f:
    pickle.dump(xgb_model, f)

Создание приложения

Раньше я не использовал streamlit, но меня очень впечатлил интерфейс и простота настройки, и я определенно буду использовать больше в будущем. Для всех этих проектов я использую IDE под названием Pycharm. Pycharm великолепен, так как он обеспечивает покрытие для ноутбуков R, Jupyter и создания приложений. Он также может синхронизироваться с github для загрузки проектов, поэтому я рекомендую использовать его всем, кто до сих пор использует только ноутбуки. Используемые библиотеки приведены ниже. Сначала я начинаю с открытия сохраненной модели и создания функции для обработки ручного ввода. Затем я создаю функцию, которая не показана здесь, для моделирования оценок на основе независимой модели Пуассона с нулевым завышением. Нулевой завышенный пуассон использует настраиваемый параметр тета, который позволяет мне взвесить вероятность 0–0, что, как мы знаем, в футболе является довольно низкой результативностью. Эта модель обычно рекомендуется вместо стандартной модели Пуассона для моделирования оценок.

import pickle
import numpy as np
import pandas as pd
import streamlit as st
from matplotlib import pyplot as plt
from scipy.stats import poisson, nbinom

# Load the saved XGBoost model
with open('xgb_model.pkl', 'rb') as f:
    xgb_model = pickle.load(f)


# Define a function to preprocess the input data
def preprocess_data(home_xG, away_xG):
    # Create a dictionary with the input data
    input_data = {'home_xG': [home_xG], 'away_xG': [away_xG]}
    # Convert the dictionary to a pandas DataFrame
    input_df = pd.DataFrame.from_dict(input_data)
    # Return the preprocessed data
    return input_df

Из эмпирического тестирования я обнаружил, что регрессор XGBoost изо всех сил пытается смоделировать события с низкой оценкой, то есть значения XG ниже 0,3. Вероятно, это связано с тем, что в наборе данных содержится меньше экземпляров игр, чем у команд были такие низкие итоги. Я также обнаружил элемент домашнего преимущества с заниженными данными при моделировании аналогичных показателей xg. Существует тенденция к тому, чтобы команда хозяев имела более высокую вероятность победы.

Ниже приведен основной код приложения, который предоставляет текстовые поля и ручной ввод для пользователя. Я закодировал исключение, которое запрещает пользователю вводить 0,00 в качестве общего xg, поскольку модель снова изо всех сил пыталась смоделировать игры без xg. Я думаю, что это снова редкое явление.

# Define the Streamlit app
def main():
    # Set the app title
    st.title('xG Match Result Prediction App')
    # Add a text box to the app
    text = st.text('Using known xG figures for a match, enter them into the boxes below.\n'
                   'The first prediction is made from an XGBoost Regressor trained on a dataset of 17000 rows from '
                   'understat.\n'
                   'There is a clear element of home advantage derived from the model as can be seen with similiar xG values.\n'
                   'The second prediction uses a ZIF Poisson model with a theta of 0.08 to account for the 0-0.')

    # Add input fields for home and away xG
    home_xg = st.number_input('Home xG', min_value=0.0, max_value=6.0, step=0.05)
    away_xg = st.number_input('Away xG', min_value=0.0, max_value=6.0, step=0.05)
    # Add a button to trigger the prediction
    if st.button('Predict'):
        if home_xg == 0.0 or away_xg == 0.0:
            st.error('Please enter an xG value for both teams.')
        else:
            # Preprocess the input data
            input_data = preprocess_data(home_xg, away_xg)
            # Make the prediction
            prediction = xgb_model.predict(input_data)[0]
            # Normalize the prediction
            prediction_normalized = prediction / sum(prediction)
            st.success("The predicted match outcomes are:\nHome team: {:.4f}\nDraw: {:.4f}\nAway team: {:.4f}".format(
                prediction_normalized[0], prediction_normalized[1], prediction_normalized[2]))
            st.success("The summed match outcomes are: {:.4f}".format(sum(prediction_normalized)))

        # Make the Poisson model prediction
        poisson_prediction = zero_inflated_poisson_model(home_xg, away_xg)


# Run the app
if __name__ == '__main__':
    main()

Развертывание

Шаги

  1. Создайте новую виртуальную среду в IDE, используя python 3.9.
  2. Установите все необходимые библиотеки в этой среде и убедитесь, что они активированы, прежде чем делать это. (много гайдов в сети)
  3. запустите приложение локально и протестируйте. Streamlit допускает развертывание после отправки проекта в репозиторий github. Вы можете сделать это в pycharm и вставить URL-адрес.
  4. Убедитесь, что файл requirements.txt предоставлен на ваш github.

и вуаля.

В заключение модель является хорошим прокси, но не точным предиктором. С появлением дополнительных данных в ближайшие несколько месяцев его производительность, вероятно, улучшится. Обе модели не зависят друг от друга, в некоторых случаях ZIF обеспечивает лучшую оценку игр с меньшим количеством очков. Тот факт, что R2 и mse/rmse настолько низки в окончательной модели, заключается в том, что я, по сути, хочу максимально воспроизвести работу недооценки. Однако это не означает, что это идеальная модель.

Если вам нравится моя работа, пожалуйста, подпишитесь на меня, чтобы получить больше подобного контента, и, пожалуйста, используйте приложение и дайте мне знать о любых отзывах. Если вы не являетесь участником и хотели бы принять участие в этом замечательном сайте, вы можете использовать мою реферальную ссылку без каких-либо дополнительных затрат для себя. Хорошего дня и удачного кодирования.



Повышение уровня кодирования

Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:

  • 👏 Хлопайте за историю и подписывайтесь на автора 👉
  • 📰 Смотрите больше контента в публикации Level Up Coding
  • 💰 Бесплатный курс собеседования по программированию ⇒ Просмотреть курс
  • 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"

🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу