Сквозное глубокое погружение для количественного исследования отбора Матчей всех звезд НБА.

Что именно нужно для того, чтобы стать Матчем звезд НБА? Как давний фанат баскетбола, это была забавная и полезная задача, в которую можно было погрузиться и исследовать.

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

Ссылка, чтобы увидеть моделирование, прогнозы и интерпретацию!

Если вы хотите перейти к делу и пропустить процесс сбора данных, перейдите к Часть 2: Моделирование, чтобы увидеть прогнозы!

Ссылка на репо!

Нажмите здесь, чтобы увидеть полное репо (с readme) на моем Github!

Контекст и мотивация

Матч всех звезд НБА - это ежегодный показательный матч, в котором участвуют 24 ведущих игрока лиги. Блестящее и гламурное мероприятие является изюминкой All-Star Weekend, и знаменитости из списка A часто собираются толпами, чтобы посмотреть на это мероприятие.

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

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

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

По сути, мы можем ожидать, что выбранные игроки будут просто консенсусом «24 лучших игрока в лиге»…, но как именно мы определяем «лучший»? Очевидно, что статистика игры, такая как очки и подборы, важна, но игровое поле не всегда бывает одинаковым. Для любого давнего фаната НБА не секрет, что командный рейтинг играет большую роль в том, как мы оцениваем игроков индивидуально; значительно сложнее заручиться уважением и признанием со стороны болельщиков и коллег, если ваша команда окажется в нижней части турнирной таблицы.

Из-за этого негативного предубеждения против проигрывающих команд многие потенциальные звезды не попали в список, например, Карл-Энтони Таунс в 2016–2017 годах, но этот эффект трудно описать количественно. Насколько уже стойки ворот для игрока в плохой команде по сравнению с хорошей? То же самое возникает и в отношении травм. Игрокам, которые пропускают продолжительное время, будет труднее получить место, даже если они выставят большие цифры во время реальной игры. Но что можно назвать «продленным временем»? Где именно наложенное ограничение, которое мы используем, чтобы принять это решение?

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

План

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

Нам нужно будет собрать исторические данные за несколько сезонов, но прежде чем мы разберем какие-либо записные книжки Jupyter, нам нужно расслабиться и подумать обо всех возможных функциях, которые будут приняты во внимание при выборе All-Star. Какая информация помогает нам принять решение для конкретного игрока?

Для начала нам понадобятся традиционные средние показатели, такие как очки, подборы, передачи и т. Д. Мы можем дополнить их некоторыми расширенными статистическими данными, включая коэффициент использования, истинный процент бросков, долю побед в защите и PIE. PIE расшифровывается как Оценка влияния игрока, версия PER (рейтинга эффективности игрока) nba.com, которая служит универсальным показателем эффективности игрока.

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

Очень важно учитывать только первую половину сезона.

Репутация и фаворитизм поклонников - важные факторы, которые необходимо учитывать, но их сложно реализовать здесь. Очистка присутствия игрока в социальных сетях на таких платформах, как Instagram и Twitter - это (а) очень сложная задача по обработке данных и (б) невозможность надежного масштабирования в обратном направлении из-за стремительно меняющегося ландшафта социальных сетей - к тому же на самом деле этого не существовало до ~ 2010 г.

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

Мы будем стремиться создать набор данных, содержащий все эти характеристики для каждого игрока НБА для каждого сезона с 1996 года (самый ранний сезон, отслеживаемый на stats.nba.com). Крайне важно, чтобы мы учитывали только первую половину сезона, поскольку именно тогда составы выбираются, и все, что после этого не имеет значения. Я выбрал крайнюю дату 21 января для каждого года, так как это примерно то время, когда начинают приниматься решения об отборе.

Наша метка вывода будет просто обозначать, был ли выбран этот игрок (1) для Матча всех звезд в этом году или нет (0). Это наша истина. Несмотря на то, что ПГС 2020 года уже состоялась и составы составов уже давно определены, я опущу текущий сезон (2019–2020 годы) из помеченного набора данных, чтобы мы могли запустить модель позже и сравнить прогнозы с известным результатом.

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

Построение набора данных

Источники данных

Stats.nba.com будет нашим основным ресурсом для удовлетворения наших требований. Он поддерживает функцию запроса статистики по настраиваемым диапазонам дат, что необходимо для того, что мы хотим сделать. Их веб-страницы являются динамическими по своей природе (AJAX), поэтому извлечение с них данных нетривиально - подробнее об этом позже.

Данные из этого ресурса относятся к сезону 1996–1997 годов, что дает нам приличный пул тренировочных данных для нашей модели.

Примечание. данные, идущие дальше этого порогового значения, определенно существуют, и хотя большее количество данных обычно дает более точные модели, закономерности между атрибутами игрока и его решением о выборе могут не быть полностью неизменными во времени. Например, зрители НБА двух разных эпох могут по-разному акцентировать внимание на защите. Так что ограничение на 1996–97 гг. Здесь более чем допустимо.

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

Обратите внимание, что в таблице игроков команды перечислены по их сокращенным обозначениям (например, CHI), но в таблице команд используются полные названия (например, Chicago Bulls). Когда мы доберемся до их объединения, нам нужно будет создать некую структуру поиска, чтобы связать их.

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

Здесь следует отметить, что мы не должны пренебрегать игроками, которые были отобраны как All-Stars, но не смогли принять участие из-за травмы. Несмотря на то, что они официально не занимали место в составе, они были выбраны изначально и будут рассматриваться как All-Stars на протяжении всего анализа вместе с их заменами.

Темп лиги довольно неуклонно увеличивался с годами по мере развития игры, поэтому мы нормализуем применимую игровую статистику, разделив на темп. Средний темп лиги по годам - ​​это то, что мы можем легко узнать из Баскетбольного справочника.

Поскольку из-за локаута в 1999 году Матча всех звезд не было, мы полностью пропустим сезон 1998–1999 годов при построении нашего набора данных.

Парсинг веб-страниц: BeautifulSoup и Selenium

Поскольку веб-страницы в Справочнике по баскетболу статичны (то есть содержимое встроено непосредственно в его HTML), мы можем просто использовать библиотеку requests для загрузки HTML-документа.

Затем мы будем использовать BeautifulSoup для анализа документа и выборочно извлекать интересующие нас данные. Используя метод pd.read_html( … ), мы можем загрузить данные в знакомый Pandas DataFrame. , и мы отправляемся на скачки.

Несмотря на то, что наш маркированный набор данных будет начинаться с сезона 1996–1997 годов, нам потребуются данные о составе Матч звезд, которые простираются намного дальше, чем это. Например, Хаким Оладжувон был активным игроком в 96-м, но его первый матч на Матч звезд был еще в 1985-м. Мы инициализируем наш сборщик ASG с 1970 года, чтобы гарантировать, что мы никого не пропустим.

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

Для тех, кому интересно, вот код, чтобы все это произошло:

from bs4 import BeautifulSoup
from collections import defaultdict
import pandas as pd
import pickle
import requests
from unidecode import unidecode
# this dictionary will map players to a set containing all the years in which they were
# selected for an all-star game, either initially or as a replacement
all_star_appearances = defaultdict(set)
# rows to ignore when iterating the roster tables
ignore_fields = set(['Team Totals', 'Reserves'])
START_YEAR, END_YEAR = 1970, 2020
# unidecode doesn't catch the accented c in Peja's last name (Stojakovic), fix it
# also overwrite any instance of Metta World Peace to Ron Artest
def fix_name(full_name):
first_name = full_name.split(' ')[0]
if first_name == 'Peja':
return 'Peja Stojakovic'
elif first_name == 'Metta':
return 'Ron Artest'
else:
return unidecode(full_name)
for year in range(START_YEAR, END_YEAR):
# no ASG played in 1999 because of the lockout
if year == 1999:
continue
print('Scraping ASG {} data...'.format(year))
# will store all the all-stars for this year
all_stars = set([])
html = requests.get('https://www.basketball-reference.com/allstar/NBA_{}.html'.format(year)).content
soup = BeautifulSoup(html, 'html.parser')
# this part was annoying - back when ASG was always East vs. West, the tables
# were encoded with id="East"/id="West" so they could be extracted more easily/reliably
# but now, you have games like Giannis vs. LeBron and the table id's are different, so I
# had to extract them by index, which is unreliable in the event that the site's design
# changes in the future
# gets rosters for team 1 and team 2
s1, s2 = soup.findAll('table')[1:3]
df1 = pd.read_html(str(s1))[0]
df2 = pd.read_html(str(s2))[0]
# get the all-stars from teams 1 and 2
for df in [df1, df2]:
for i, row in df.iterrows():
if pd.notnull(row[0]) and row[0] not in ignore_fields:
player = row[0]
all_stars.add(fix_name(player))
# gets all li elements in the page
s3 = soup.findAll('li')
# finds the li element that contains the data pertaining to injury related selections
# - players who were selected but couldn't participate due to injury,
# and their respective replacements
#
# since all_stars is a hashset, we don't need to worry about accidentally double counting an all-star
for s in s3:
if 'Did not play' in str(s):
for player in [name.get_text() for name in s.findAll('a')]: # all the injured players and their replacements
all_stars.add(fix_name(player))
break
# update the appearances dictionary
for player in all_stars:
all_star_appearances[player].add(year)
sorted_all_star_appearances = sorted([(player, sorted(list(appearances))) for player, appearances in all_star_appearances.items()], key = lambda x : -len(x[1]))
print('\nAll all-star appearances since 1970 (sorted by number of appearances):\n')
for player, appearances in sorted_all_star_appearances:
print('{}: {}'.format(player, appearances))
# export the dictionary to local disk for future recall in statsnba_fullscrape.py
out = open('all_star_appearances.pickle', 'wb')
pickle.dump(all_star_appearances, out)
out.close

На следующем этапе нам нужно очистить табличные данные с stats.nba.com, и здесь все становится немного сложнее. Эти веб-страницы имеют динамическую реализацию с использованием AJAX (асинхронный JavaScript и XML), поэтому отображаемые таблицы нигде не встречаются в документе HTML.

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

Итак, у нас есть два реальных варианта:

  • используйте для этого встроенную утилиту, например nba-api, прочитав всю документацию, чтобы изучить функциональные возможности.
  • использовать автоматизированное программное обеспечение браузера, такое как Selenium WebDriver

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

Selenium - это фреймворк, который позволяет нам вызывать автоматический браузер (WebDriver), а с помощью API в Python мы можем управлять этим браузером для перехода на соответствующую веб-страницу. После периода задержки, как только будет получен ответ от серверной части, мы, наконец, можем загрузить HTML-документ (содержащий важные данные) и использовать BeautifulSoup и Pandas, как и раньше.

Есть еще несколько препятствий, с которыми я столкнулся во время этого соскабливания, но большинство из этих проблем были по своему характеру ограниченными (например, Charlotte Hornets упоминались как CHH в эпоху до Bobcats и CHA впоследствии) и объясняются в комментариях.

Скрипт будет очищать stats.nba.com для каждого игрока НБА за каждый сезон с 1996 года по сегодняшний день. Наборы данных обрабатываются и объединяются соответствующим образом, а затем дополняются с помощью справочного словаря All-Star, который мы создали ранее. Два набора данных экспортируются в формате CSV: один содержит все помеченные данные за период с 1996–1997 годов по 2018–1919 годы, а другой - немаркированные данные за текущий сезон.

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

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

Полный список необработанных функций:

  • Год (календарный год, в котором начался сезон)
  • Средн. Темп (средний темп в лиге в этом сезоне)
  • ИГРОК
  • КОМАНДА
  • Рейтинг команд конференции
  • GP (сыгранные игры)
  • Team GP (сыгранные командные игры)
  • W (выигранные игры - игрок должен был сыграть)
  • PTS (очков за игру)
  • РЭБ (подборов за игру)
  • AST (голевых передач за игру)
  • STL (перехватов за игру)
  • BLK (блоков за игру)
  • ТОВ (обороты за игру)
  • TS% (истинный процент стрельбы)
  • 15:00 (3 указателя за игру)
  • DEFWS (доля выигрыша в обороне)
  • USG% (коэффициент использования)
  • PIE (оценка влияния игрока)
  • Предыдущие выступления в ASG
  • КАК В прошлом году?

Формальные определения этой игровой статистики можно найти на https://stats.nba.com/help/glossary/.

Скоро…

В Части 2 этого глубокого погружения мы рассмотрим моделирование процесса отбора звезд НБА с использованием созданного нами набора данных. Спасибо за прочтение!