В этой статье рассказывается о подходе к Kaggle 2023 March Machine Learning mania, соревнованию по науке о данных, призванному предсказать результаты матчей мужских и женских баскетбольных турниров колледжей.

Введение

Каждый год, когда наступает март, есть определенные вещи, к которым люди привыкли: запах ранних весенних цветов, сытная еда из солонины и Гиннеса на День Святого Патрика, а также Уоррен Баффет, предлагающий свое ежегодное предложение в 1 миллиард долларов для всех. который может правильно предсказать результаты турнирной сетки March Madness для студенческого баскетбола. Начиная с 2014 года, Баффет вместе с Quicken Loans объявили об этом испытании для всех, кто сможет выполнить идеальную сетку до начала игр. Конечно, по сей день, 7 лет спустя, никто не смог обналичить чек на миллиард долларов. Когда вы начинаете смотреть на шансы, неудивительно, почему.

Учитывая турнир, который начинается с 64 команд и заканчивается 1 победителем, есть 6 раундов игры и 2⁶³ возможных итераций сетки (другими словами, 9,2 квинтиллиона). Если бы каждый человек на Земле (примерно 8 миллиардов) сделал 1,15 миллиарда брекетов, то вы могли бы гарантировать, что один из них — идеальный брекет. Излишне говорить, что Баффет знал, что делает беспроигрышную ставку, когда начинал соревнование в 2014 году.

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

Кроме того, с турнирами March Madness 2023 года в зеркале заднего вида на момент написания этой статьи мы можем сказать, что любой модели было бы труднее предсказать результаты в этом году. Например, в мужском турнире университет Fairleigh Dickinson University (FDU), посеянный под номером 16, опередил команду Purdue Boilermakers, посеянную под номером 1, став лишь второй школой в истории турнира, которая опередила посевную команду, занявшую первое место. Кроме того, в раунде элитной восьмерки не было лучшего посева. В женских турнирах также было немало сюрпризов, таких как Университет Майами № 9, пробившийся в элитную восьмерку.

Использование машинного обучения для прогнозирования турнира March Madness

Мартовское соревнование «Безумие машинного обучения» 2023 года предоставило участникам множество источников данных, которые помогли построить модели для прогнозирования результатов игр турниров. Они предоставили командную статистику, результаты игр, места проведения игр, тренеров, места проведения и другую информацию за несколько десятилетий, чтобы помочь конкурентам построить свои модели.

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

Одна из вещей, которую я быстро заметил при анализе данных и применении моделей машинного обучения, заключается в том, что семена для турнира играют самую важную роль в определении результатов, на самом деле более высокие семена выигрывают в 71,92% случаев, начиная с 1985 года. , Это важно отметить, но это также должно послужить предупреждением для ученых, занимающихся данными, чтобы умерить свою зависимость от посева с другими факторами, чтобы сделать вашу модель более устойчивой к расстройствам и выступлениям Золушки.



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

Процесс

Чтобы описать процесс:

  1. Импортируйте библиотеки и данные
  2. Предварительно обработайте данные, чтобы сделать их более полезными для модели.
  3. Создайте обучение и набор данных для проверки
  4. Создайте модель с помощью XGboost
  5. Обучите и проверьте модель на данных, используя метод перекрестной проверки K-fold.
  6. Используйте обученную модель для прогнозирования каждой возможной игры в мужских и женских турнирах March Madness.

Чтобы просмотреть полный код, следуйте блокноту Jupyter на Kaggle.



Импортируйте библиотеки и данные

# Data processing libraries
import glob
import pandas as pd
import numpy as np
import math
import random
import matplotlib.pyplot as plt
import re

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

  • была предоставлена ​​статистика по общему количеству совершенных и предпринятых попыток

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

# Separate and reconnect the winning and losing teams
rename_winner_cols = {col: col[1:] for col in regular_season_df.columns if col[0] == "W"}
rename_loser_cols = {col: col[1:] for col in regular_season_df.columns if col[0] == "L"}

teams_df = pd.concat([
    pd.read_csv(paths["MTeams"]),
    pd.read_csv(paths["WTeams"])
], ignore_index=True).drop(["FirstD1Season", "LastD1Season"], axis=1)


_winning_df = regular_season_df.copy()
_winning_df["PtDiff"] = _winning_df["WScore"] - _winning_df["LScore"]
_winning_df["PtsAllowed"] = _winning_df["LScore"]
_winning_df["FGM/FGA_Allowed"] = _winning_df["LFGM"] / _winning_df["LFGA"]
_winning_df["Win"] = 1
_winning_df = _winning_df.rename(rename_winner_cols, axis=1).drop(rename_loser_cols.keys(), axis=1)

winning_teams_df = pd.merge(
    left=teams_df,
    right=_winning_df,
    on="TeamID"
)

_losing_df = regular_season_df.copy()
_losing_df["PtDiff"] = _losing_df["LScore"] - _losing_df["WScore"]
_losing_df["PtsAllowed"] = _losing_df["WScore"]
_losing_df["FGM/FGA_Allowed"] = _losing_df["LFGM"] / _losing_df["LFGA"]
_losing_df["Win"] = 0
_losing_df = _losing_df.rename(rename_loser_cols, axis=1).drop(rename_winner_cols.keys(), axis=1)

losing_teams_df = pd.merge(
    left=teams_df,
    right=_losing_df,
    on="TeamID"
)

teams_df = pd.concat([
    winning_teams_df,
    losing_teams_df
], ignore_index=True).sort_values(["Season", "TeamID", "DayNum"]).reset_index(drop=True)

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

Совокупная статистика рассчитывалась путем группировки команд по сезонам.

# Save win, tourney and score column to reattach
win_col = teams_df["Win"]
score_col = teams_df["Score"]
tourney_col = teams_df["Tourney"]

cumulative_cols = ['Win', 'Score', 'PtDiff', 'FGM', 'FGA', 'FGM3', 'FGA3', 'FTM', 'FTA', 'OR', 'DR',                    
                   'Ast', 'TO', 'Stl', 'Blk', 'PF', 'PtsAllowed', 'FGM/FGA_Allowed']

cum_sum = teams_df.groupby(["Season", "TeamID"])[cumulative_cols].cumsum()
teams_df = teams_df[["TeamID", "TeamName", "Season", "DayNum"]]
teams_df[cumulative_cols] = cum_sum
teams_df = teams_df.rename({ "Win": "CumWins", "Score": "CumScore" }, axis=1)

# reattach cols
teams_df["Win"] = win_col
teams_df["FinalScore"] = score_col
teams_df["Tourney"] = tourney_col

count = teams_df.groupby(["Season", "TeamID"]).cumcount() + 1
teams_df["count"] = count

def calculatePerGameStatistics(df, category):
    df[f"{category}/g"] = df[category] / df["count"]
    if category == "CumScore":
        df.rename({"CumScore/g": "Pts/g"}, axis=1, inplace=True)
    if category == "CumWins":
        df.rename({"CumWins/g": "WPct"}, axis=1, inplace=True)

# Create per game statistics for the following categories
for category in ["CumWins", "PtDiff", "PtsAllowed", "CumScore", "OR", "DR", "TO", "Stl", "Blk", "Ast", "PF", "FGM/FGA_Allowed"]:
    calculatePerGameStatistics(teams_df, category)


def calculatePercentageStatistics(df, cat1, cat2):
    df[f"{cat1}/{cat2}"] = df[cat1] / df[cat2]
    df = df.drop([cat1, cat2], axis=1)

# Create percentage statitistics for the following categories
for cat1, cat2 in [("FGM", "FGA"), ("FGM3", "FGA3"), ("FTM", "FTA")]:
    calculatePercentageStatistics(teams_df, cat1, cat2)

def treatSeed(seed):
    return int(re.sub('[^0-9]', "", seed))

teams_df = pd.merge(
    teams_df,
    seeds_df,
    how='left',
    left_on=["Season", "TeamID"],
    right_on=["Season", "TeamID"]
)

teams_df["Seed"] = teams_df["Seed"].fillna('X17')
teams_df["Seed"] = teams_df["Seed"].apply(treatSeed)
teams_df = teams_df.drop_duplicates()

*Чтобы данные для каждой игры были выровнены, после этого шага номер дня был сдвинут на -1.

Прежде чем можно будет заполнить модель, команды должны собраться вместе для каждой игры после того, как будет получена их совокупная статистика. Чтобы сделать это беспристрастным образом, каждая команда была случайным образом назначена либо командой A, либо командой B. Затем был предсказан результат с точки зрения того, выиграет ли команда A или проиграет. Кроме того, чтобы упростить ввод признаков в модель, признаки вводились как разница между совместимой статистикой для команды А и команды Б. Например, для модели была указана разница в очках, а не очки за игру для команды А и очки за игру. для команды Б.

Создание наборов данных для обучения и проверки, построение и настройка модели

Модели обучения и проверки были выбраны случайным образом с помощью метода перекрестной проверки K-fold. Разделение обучения/проверки было 80/20.

Использовалась модель XGBRegressor с 10 оценщиками. XGB — это регрессионная модель с экстремальным повышением градиента, в которой для уточнения прогнозов используется метод ансамбля случайных деревьев решений. Я использовал этот метод, потому что он быстрее и эффективнее, чем нейронная сеть, для данных, не являющихся изображениями, но очень похож на нейронную сеть, которая имеет очень высокую производительность.

from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score

def KFolds(df,  df_test=None, df_submission=None, folds=None, mode='reg'):
    seasons = df['Season'].unique()
    cvs = []
    pred_tests = []
    pred_subs = []
    target="ScoreDiff" if mode == 'reg' else 'Win'
    scaler = StandardScaler()
    fold = 0
    
    minScore = float("inf")
    best_index = 0
    
    maxAccScore = float(0)
    best_acc_index = 0
    
    for fold in range(folds):
        print(f'\n Validation on fold: {fold + 1}')
        
        
        # For random folds
        val_size = round(len(df) * 0.2)
        val_indices = random.choices(range(len(df)), k=val_size)
        train_indices = list(set(range(0, len(df))) - set(val_indices))
        df_train = df.iloc[train_indices]
        df_val = df.iloc[val_indices]     
        
        X_train, X_val = df_train[features], df_val[features]
        y_train, y_val = df_train[target], df_val[target]
        X_train, X_val = scaler.fit_transform(X_train), scaler.fit_transform(X_val)
        
        if mode == 'reg':
            model = xgb.XGBRegressor(n_estimators=10, seed=123)
        else:
            model = xgb.XGBClassifier()
        
        model.fit(X_train, y_train)
        
        if mode == "reg":
                pred = model.predict(df_val[features])
        else:
            pred = model.predict_proba(df_val[features])[:, 1]
        
        if df_test is not None:
            if mode == 'reg':
                pred_test = model.predict(df_test[features])
                pred_test = (pred_test - pred_test.min()) / (pred_test.max() - pred_test.min())
            else:
                pred_test = model.predict(df_test[features])
            pred_tests.append(pred_test)
            
            accuracy = accuracy_score(pred_test, df_test[target])
            maxAccScore = max(maxAccScore, accuracy)
            if df_submission is not None:
                if mode == 'reg':
                    pred_submission = model.predict(df_submission[features])
                    pred_submission = (pred_submission - pred_submission.min()) / (pred_submission.max() - pred_submission.min())
                else:
                    pred_submission = model.predict(df_submission[features])
                pred_subs.append(pred_submission)

            if accuracy == maxAccScore:
                best_acc_index = fold

            print(f'Accuracy: {accuracy}, Best Accuracy: {maxAccScore}, Best Accuracy Fold: {best_acc_index+1}')
            
        
        pred = pred if df_test is not None else 0.5
            
        pred = (pred - pred.min()) / (pred.max() - pred.min())
        pred = np.clip(pred, 0, 1)
        
        score = ((df_val['Win'].values - pred) ** 2).mean()
        cvs.append(score)
        
        if score < minScore:
            best_index = fold
            minScore = score
        print(f'\t Best Fold: {best_index+1}')
        
        print(f'\t -> Scored {score:.3f}')
        fold += 1
    print(f'\n Local CV is {np.mean(cvs):.3f}')
    
    return pred_tests, best_index, pred_subs, best_acc_index

Применение модели к данным тестирования

Чтобы применить эту модель к данным тестирования, была рассчитана средняя статистика за сезон 2023 года, а предполагаемый посев был взят с сайта espn.com за неделю до турнира. Затем модель могла бы делать прогнозы для каждого возможного матча в турнирах March Madness.

Результаты

Модель, которую я реализовал, имела точность 71,5% для турниров этого года.

Движение вперед

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