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

Вы были наняты в качестве координатора защиты для Далласских Ковбоев, и вы тренируете против Патриотов Новой Англии в Суперкубке. Я понимаю, что это очень маловероятный сценарий, учитывая, что «Ковбои» совсем не похожи на претендентов на Суперкубок. Однако мы все еще можем использовать это как пример проекта для шагов по созданию модели AI / ML.

Патриоты были лучшей командой НФЛ за последние два десятилетия, а Том Брэди, квотербек Патриотов, возможно, был лучшим игроком НФЛ за тот же период. Как координатор обороны, остановить Патриотов будет огромной задачей. Однако у нас есть преимущество: AI / ML. Давайте рассмотрим процесс, чтобы увидеть, можем ли мы предсказать ход игры до того, как она произойдет, что позволит нам соответствующим образом скорректировать нашу схему защиты. В этом примере мы будем использовать Python и sklearn. Вот как мы будем следовать этому процессу:

  1. Получать или генерировать данные
  2. Подготовьте данные для исследовательского анализа данных (EDA).
  3. Проанализируйте данные, чтобы найти потенциальные закономерности.
  4. Подготовьте данные для моделей AI / ML.
  5. Обучите и оцените выбранную вами модель AI / ML.

Получение или создание данных

Первое, что вам понадобится, это набор данных. Если у вас нет доступа к соответствующему набору данных, вы можете создать образец набора данных, используя такой процесс, как эта статья. В нашем примере мы будем использовать этот набор данных, содержащий информацию о каждой игре НФЛ, проводимой с 2009 года. Он размещен на Kaggle. Если вы не знакомы с Kaggle, это сайт конкурса программистов с множеством разных наборов данных; Обычно это первое место, которое я проверяю, когда хочу найти какие-то данные.

После того, как мы загрузили и разархивировали файл, он нам понадобится в формате, который позволяет нам исследовать данные. Для этого мы будем использовать Pandas, библиотеку Python, которая содержит множество функций для изменения и изучения набора данных. Вот код для загрузки фрейма данных из нашего CSV-файла:

import pandas as pd
import matplotlib
from sklearn import preprocessing
%matplotlib inline
df = pd.read_csv('data/nfl_plays.csv')

Подготовьте данные для исследовательского анализа данных (EDA)

EDA - это процесс изучения данных на предмет проблем (отсутствующие значения, неправильно набранные данные и т. Д.), И, надеюсь, мы сможем увидеть некоторые тенденции в данных.

Во-первых, давайте посмотрим, сколько строк и столбцов мы загрузили:

df.shape

который дает:

(449371, 255)

Как видите, в этом наборе данных много информации (255 столбцов). Было бы неплохо знать, что представляет собой каждое поле. Потребовалось немного покопаться, но в итоге я нашел эту страницу, которая описывает все столбцы и их значение. Меня интересуют эти поля:

  • posteam - команда, владеющая мячом (в данном случае нам нужен только NE)
  • game_seconds_remaining - сколько времени осталось в игре
  • ярдлайн_100 - на какой ярдной линии находится команда (до 100)
  • вниз - что вниз мы на (1.0, 2.0, 3.0, 4.0)
  • ydstogo - ярдов, необходимых для первого падения
  • дробовик - они выстроились в строй?
  • score_differential - их счет - наш счет
  • play_type - тип игры (бег, пас и т. д.)
  • run_location - куда они бежали (слева, посередине, справа)
  • pass_location - каким путем они прошли (слева, посередине, справа)

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

#filter rows
nedf = df[(df.posteam=='NE') & (df.down.isin([1.0, 2.0, 3.0, 4.0])) & ((df.play_type=='run') | (df.play_type == 'pass'))]
#filter columns
nedf = nedf[['game_seconds_remaining', 'yardline_100', 'down', 'ydstogo', 'shotgun', 'score_differential', 'play_type', 'pass_length', 'pass_location', 'run_location']]
nedf.head()

На этом этапе у нас есть еще один шаг, прежде чем мы сможем начать EDA. Значение, которое мы пытаемся предсказать, представлено четырьмя столбцами, которые зависят от play_type. Например:

  • Если тип воспроизведения - «запуск», run_location будет заполнен, но pass_location не будет.
  • Если тип воспроизведения - «пройти», pass_location будет заполнен, а run_location - нет.

Нам нужно объединить все эти значения в одно поле. Вот код для этого (обратите внимание, мы также отфильтровываем location = ’unknown’):

import numpy as np
def get_full_play_type(play):
    play_type, pass_location, run_location = play
    if(play_type == 'run'):
        return play_type+'_'+ run_location
    else:
        return play_type+'_'+ pass_location
nedf = nedf.replace(np.nan, 'unknown', regex=True)    
nedf['full_play_type'] = nedf[['play_type','pass_location', 'run_location']].apply(get_full_play_type, axis=1)
nedf = nedf[(nedf.full_play_type.isin(['pass_left', 'pass_middle','pass_right','run_left', 'run_middle', 'run_right']))]

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

nedf.head()

Анализируйте данные для поиска потенциальных закономерностей

Во-первых, давайте посмотрим, какие значения содержит наш full_play_type:

nedf.groupby(['full_play_type']).count()[['play_type']]

Как видите, если бы мы случайно угадали следующий ход, у нас был бы один из шести шансов (16,66%), что мы ошибаемся. Мы будем использовать это в качестве основы, чтобы увидеть, сможем ли мы улучшить.

Иногда помогает просмотреть это в формате диаграммы:

nedf.groupby(['full_play_type']).count()[['play_type']].plot(kind='bar')

Теперь давайте разберемся, какие пьесы называются в процентах от общего количества:

nedf.groupby(['full_play_type']).count()[['play_type']].apply(lambda x:100 * x / float(x.sum()))

Наши прогнозы уже улучшились! Если мы прогнозируем pass_left для каждой игры, мы будем правы в 23% случаев по сравнению с 16,66% случаев. Мы все еще можем добиться большего.

Подготовьте данные для моделей AI / ML

Для моделей AI / ML требуются числовые входы и выходы. Они также работают лучше, когда шкала чисел похожа. В этом разделе мы будем использовать несколько различных методов для достижения этой цели.

Сначала мы воспользуемся библиотекой предварительной обработки sklearn, чтобы преобразовать наши full_play_types в числовые:

le = preprocessing.LabelEncoder()
le.fit(nedf.full_play_type)
nedf['full_play_type_code'] = le.transform(nedf['full_play_type'])

Это создает столбец с именем full_play_type_code, который является числовым представлением full_play_type. Давайте посмотрим на данные full_play_type_code и убедимся, что они по-прежнему соответствуют full_play_type.

nedf.groupby(['full_play_type_code']).count()[['down']].plot(kind='bar')

Как видите, диаграмма совпадает с приведенной выше, но теперь ось x представляет собой числовое значение, а не текст.

Далее я хочу упростить значение ydstogo. С технической точки зрения это может быть значение от 1 до 99. Однако я хочу разбить это расстояние на несколько сегментов (1–4 ярда, 5–8 ярдов, 9–12 ярдов, 13–16 ярдов, ≥17 ярдов), которые должны позволить нашей модели идентифицировать узоры. Выбранные мной диапазоны произвольны и могут быть легко изменены.

def bucketize(val, size, count):
    i=0
    for i in range(count):
        if val <= (i+1)*size:
            return i
    return i
def bucketize_df(df):
    df['ydstogo'] = [bucketize(x, 4, 5) for x in df['ydstogo']]
    return df
nedf = bucketize_df(nedf)

Теперь я хочу выполнить горячее кодирование столбцов down и ydstogo, что означает, что мы «переместим» строки в столбцы и заполним столбец, содержащий значение, которое мы кодируем, «1», иначе - нулями. Например, в футболе четыре падения. Когда мы горячо кодируем нижний столбец, мы получаем четыре столбца, в которых один будет заполнен только один раз. Если бы мы выполняли горячее кодирование строки, представляющей третью вниз, мы бы получили эти значения для наших новых столбцов [0, 0, 1, 0]. Обратите внимание, что третий столбец - единственный, который содержит один. Преимущество этого заключается в том, что наши данные down и ydstogo теперь представлены в виде нулей и единиц, что также должно помочь нашей модели в обучении. Код для этого в sklearn очень прост:

nedf = pd.concat([nedf, pd.get_dummies(nedf['down'], prefix='down')], axis=1)
nedf = pd.concat([nedf, pd.get_dummies(nedf['ydstogo'], prefix='ydstogo')], axis=1)

Вот как выглядит образец наших данных на данный момент:

Теперь вы можете видеть, что ydstogo_ * и down_ * представлены нулями и единицами. Однако столбцы game_seconds_remaining, ярдлайн_100 и разность очков по-прежнему имеют шкалу, отличную от нуля и единицы. Например, оставшиеся секунды игры содержат значение от нуля до 3600.

Мы можем исправить это, добавив математики в столбцы. Сначала давайте посмотрим, какие диапазоны значений содержат эти столбцы.

nedf.describe()

В этом упражнении мы будем смотреть на минимальные и максимальные значения и использовать их для нормализации данных до значения от нуля до единицы. Минимальное значение для game_seconds_remaining равно 3, а максимальное - 3600. Мы разделим game_seconds_remaining на 3600. Yardline_100 будет разделено на 100. Для разницы в счете мы применим следующую формулу:

(score_diff + min_diff) / (max_diff-min_diff)

Однако для упрощения мы будем использовать -50 для min_diff и 50 для max_diff. Наша логика такова, что если вы выигрываете или проигрываете более чем на 50 очков, у вас на данный момент другие мысли. Вот наш код для применения этих формул:

nedf['game_seconds_remaining']/=3600
nedf['yardline_100']/=100
nedf['score_differential']=(nedf['score_differential']+50)/100

Теперь, если мы посмотрим на наш набор данных, вот что мы получим:

Теперь у нас есть все поля ввода, преобразованные в значения от нуля до единицы, поэтому мы готовы опробовать это на модели.

Обучите и оцените выбранную модель AI / ML

В этом уроке мы будем использовать RandomForestClassifier, чтобы посмотреть, сможем ли мы предсказать следующую игру. Но сначала нам нужно избавиться от всех столбцов, которые нам не нужны для нашей модели. Затем нам нужно разделить наши данные на поезд и тестовый набор, каждый из которых содержит X (входные данные) и y (данные результата). Это можно сделать быстро с помощью трех строк Python.

from sklearn.model_selection import train_test_split
#select important columns for input
X=nedf[['yardline_100', 'shotgun', 'score_differential', 'game_seconds_remaining', 'down_1.0', 'down_2.0', 'down_3.0', 'down_4.0','ydstogo_0','ydstogo_1','ydstogo_2','ydstogo_3','ydstogo_4']]
#select result column for output
Y=nedf['full_play_type_code']
#split data for train and test
train_x, test_x, train_y, test_y = train_test_split(X, Y, random_state = 0)

На этом этапе мы можем обучить нашу модель соответствовать входным данным.

from sklearn.ensemble import RandomForestClassifier
the_clf=RandomForestClassifier(max_depth=8, n_estimators=64)
the_clf.fit(train_x, train_y)

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

from sklearn.metrics import accuracy_score
pred = the_clf.predict(test_x)
acc =accuracy_score(test_y, pred)
print(acc)

Теперь мы можем предсказать правильную игру в 31% случаев, что может показаться не очень хорошим, но имейте в виду, что это почти вдвое больше, чем мы начали со случайного угадывания (16,66%), и на 25% лучше, чем просто угадывать игру, которая им нравится. чтобы работать больше всего (pass_left, 23%).

Следующие шаги

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

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

  1. Передискретизация / увеличение данных
  2. Выбор модели
  3. Оценка модели
  4. Тюнинг модели

Спасибо за внимание, код этой статьи будет размещен на github.