Использование Python с Pandas и Plotly для нарезки, группировки и визуализации данных для получения новых идей (код и данные в моем репозитории GitLab).

В этой статье я демонстрирую, как обрабатывать и визуализировать данные для сравнительного анализа. Я начинаю анализ с одного набора данных, который будет проанализирован, чтобы найти подходящее разделение и найти лучшее подмножество для сравнения с оставшимися данными.
Фактически, в этой статье рассматриваются данные НБА за прошлый сезон, чтобы узнать о близких и поздних игровых ситуациях. Некоторые, например, Билл Симмонс из The Ringer, называют это кризисным временем.
Мы будем использовать данные, чтобы узнать больше об этих важных моментах. Когда действительно наступает время кризиса? По-другому играется в этот период? Какие стратегии здесь приняты?
Как обычно, основное внимание в статье будет уделяться анализу данных, который будет применим к вашей собственной области и данным, а не результатам, связанным с баскетболом.
Давайте идти.
Прежде чем мы начнем
Данные и код
Я включил данные и код для этого в свое репозиторий GitLab здесь (в каталоге Basketball_crunchtime), поэтому, пожалуйста, загрузите его и поиграйте с ним / улучшите его.
Пакеты
Полагаю, вы знакомы с питоном. Тем не менее, даже если вы относительно новичок, это руководство не должно быть слишком сложным.
Вам понадобятся pandas и plotly. Установите каждый (в виртуальной среде) с помощью простого pip install [PACKAGE_NAME].
Создание данных «кранча»
Гипотеза здесь довольно проста. В баскетболе к одним ситуациям относятся иначе, чем к другим, и игроки и тренеры уделяют им больше внимания и усилий. Давайте назовем это «время кризиса» и посмотрим, обнаруживаются ли в данных какие-либо различия и закономерности между ними и остальной частью игры.
Наш набор данных включает данные по каждой игре за сезон 2018–19. Как вы могли догадаться, в данных, которые я могу отфильтровать, отсутствует функция «время обработки». Но, исходя из нашей интуиции, кажется разумным, что данные о времени кризиса будут относиться к периодам с ограниченным оставшимся временем и небольшими различиями в оценках.
Во-первых, мы исследуем данные и посмотрим, какие могут быть некоторые разумные критерии, по которым можно выбрать подмножество времени кризиса.
Создавайте новые столбцы в пандах
Существующий набор данных не включает функции для оставшегося времени или разницы в оценках, но он включает базовые данные для этих функций.
Есть несколько способов создания новых столбцов в Pandas, но я предпочитаю использовать метод .assign, поскольку он возвращает совершенно новый объект. Это может помочь избежать головной боли, связанной с возможным манипулированием неправильным объектом.
Функции набора данных включают period, из 4 и elapsed время в каждом периоде (из 12 минут). Данные period представлены целыми числами, а время elapsed - строками, поэтому мы хотели бы преобразовать их в согласованные единицы времени.
Pandas имеет timedelta функцию, которая может легко преобразовывать строки во время, а timedelta64 функция numpy может преобразовывать период в секунды, поэтому их суммирование дает нам общее время, прошедшее в игре.
Давайте также создадим новый столбец для текущей разницы в счете:
shots_df = shots_df.assign(tot_time=(shots_df.period-1)*np.timedelta64(60*12, 's') + pd.to_timedelta(shots_df.elapsed, unit='s')) shots_df = shots_df.assign(score_diff=abs(shots_df.home_score-shots_df.away_score))
Если max(shots_df.elapsed) давал ‘0:12:00', max(shots_df.tot_time) теперь дает Timedelta(‘0 days 00:48:00’). (На данный момент я исключил сверхурочные часы)
Фильтрация данных
Эти новые столбцы теперь можно использовать для фильтрации нашего набора данных. Мы будем использовать их, чтобы сгенерировать несколько потенциальных подмножеств времени кранча для сравнения.
Прежде всего, я хотел бы посмотреть, изменится ли распределение выстрелов. В более ранней статье мы разделили корт на несколько зон, например:

Каждый снимок кодируется функцией, которая фиксирует, из какой из этих 7 зон он был сделан.
Изменения места выстрела могут иметь такое же значение, как и любые изменения точности. На самом деле это могло быть больше, поскольку данные могли быть менее "зашумленными", чем точность выстрела.
Итак, давайте также определим, изменилось ли распределение выстрелов из этих зон, добавив статистику, такую как точность / частота выстрелов из каждой зоны, в наш фрейм данных.
Чтобы оценить влияние различных пороговых значений времени и оценок, я построил вложенный цикл с различными порогами времени и оценки и использовал панды для фильтрации фрейма данных на основе этих значений внутри цикла:
for time_thresh in [12, 10, 8, 6, 4, 2]:
for score_thresh in [2, 4, 6, 8, 10]:
filt_shots_df = shots_df[
(shots_df.tot_time > np.timedelta64(60*(48-time_thresh), 's'))
& (shots_df.score_diff <= score_thresh)
]
Каждая статистика также фиксируется как относительное значение по сравнению с общими данными, так как мы стремимся фиксировать изменения в игре во время кризиса.
Если вы не знаете, как использовать методы .groupby с фреймами данных, вот документация.
Это дает нам работоспособный фреймворк для сравнений, и мы готовы приступить к просмотру наших наборов данных.
Визуализация данных «кранча»
Проверка на вменяемость - размеры выборки
Когда я имею дело с подмножеством данных, я хочу опасаться внесения случайных ошибок или непреднамеренных предубеждений, которые могут привести меня к ошибочным выводам.
Давайте быстро посмотрим на распределение, чтобы понять, сколько денег в каждой игре уходит на каждый диапазон разницы в счете.
Наша база данных содержит примерно 217 000 бросков за весь сезон НБА. Ориентировочно каждая минута содержит 4500 выстрелов. Нанесение их на гистограмму:
import plotly.express as px
fig = px.histogram(shots_df, x='score_diff', nbins=40, histnorm='probability density', range_y=[0, 0.09])
fig.update_layout(
title_text='Score differences in an NBA game',
paper_bgcolor="white",
plot_bgcolor="white",
xaxis_title_text='Score difference', # xaxis label
yaxis_title_text='Probability', # yaxis label
bargap=0.2, # gap between bars of adjacent location coordinates
)
fig.show()

И аналогично последние 6 минут:

Даже за последние 6 минут около 4,5% игр НБА играются со счетом в пределах 1 очка и около 10% - в пределах 3.
При базе более 215000 снимков получение этих подмножеств кажется разумным, но за этим стоит следить, особенно потому, что подмножества выводятся на основе времени и оценок.
Определение времени кризиса
Нас интересовали две переменные: оставшееся время игры (time_thresh) и разница в счете (score_thresh). У нас также есть переменная области выстрела (zone_name). С тремя переменными, которые необходимо учитывать, он идеально подходит для пузырьковой диаграммы с одной переменной вдоль оси x, другой в качестве цветов, а третий для подзаголовков.
С помощью этого кода мы создаем одну такую пузырьковую диаграмму:
fig = px.scatter(
summary_df, x='score_thresh', y='rel_shots_freq', color='time_thresh', size='filt_shots_freq',
facet_col='zone_name', hover_data=['filt_shots_taken'])
fig.show()

И вспомогательный сюжет / цветовой ряд можно перевернуть следующим образом, чтобы получить:
fig = px.scatter(
summary_df, x='time_thresh', y='rel_shots_freq', color='score_thresh', size='filt_shots_freq',
facet_col='zone_name', hover_data=['filt_shots_taken'])
fig.show()

Эти сюжеты уже говорят нам о многом.
Они говорят нам, что в поздней игре делается гораздо больше трехочковых бросков, чем в остальной части игры. Это увеличивается по мере увеличения разницы в счете (т.е. когда команды берут на себя больше рисков, чтобы наверстать упущенное).
Современная аналитика подсказывает, что лучшие удары делаются с близкого расстояния до обода и тройки в углу, и здесь данные показывают, что таких ударов становится сложнее. Предположительно, это происходит из-за того, что защита прилагает больше усилий, особенно на очень поздних этапах игры, когда она близка (посмотрите на данные игры с фиолетовыми ‹2 очками).
Внимательный взгляд на нижний график также указывает на точку перегиба на отметке около 8 или 6 минут на большинстве участков. Это наиболее очевидно в первом подзаговоре (в пределах 4 футов) и пятом (короткие 3s).
Давайте посмотрим на точность выстрела. Рассказывает ли он нам похожую историю?

Вроде бы дело! По мере увеличения запаса очков точность имеет тенденцию увеличиваться при выстреле с близкого расстояния и снижаться при стрельбе издалека. Скорее всего, это связано с тем, что ведущая команда опасается трех указателей, чтобы их противники не догоняли быстро, а также из-за того, что команда, стоящая за спиной, чаще поднимается с дальней дистанции. Это подтверждается частотой, показанной в диаграммах выше.
В целом, многие из этих сюжетных линий указывают на то, что что-то изменилось примерно за 6 минут до конца игры. Кажется, это хороший порог.
А как насчет счета? Несмотря на то, что наблюдается прогрессивное влияние на данные по мере того, как мы смотрим на данные из все более значительных разрывов в оценках, по моим наблюдениям, точки данных для ситуаций, когда оценка находится в пределах 2, выделяются среди остальных в большинстве этих подзаголовков. (Посмотрите сами, дайте мне знать, если вы не согласны.)
Давайте возьмем такое определение времени обработки, когда оставшееся время составляет 6 минут или меньше, а оценка находится в пределах 2 баллов.
Время сжатия по сравнению с обычным (плавным?) Временем
Теперь, когда мы установили параметры для времени обработки, мы можем создать новый столбец, чтобы различать два набора данных.
shots_df = shots_df.assign(
crunchtime=(shots_df.tot_time > np.timedelta64(60*(48-6), 's'))
& (shots_df.score_diff <= 2))
crunch_df содержит 3886 выстрелов, что не на что чихать. (Но мы, вероятно, не хотели бы становиться намного меньше.)
Давайте сравним два набора данных на основе этого свойства. Теперь это легко сделать, просто построив данные вокруг логических значений переменной crunchtime:
for crunch_bool in [True, False]:
filt_shots_df = shots_df[shots_df.crunchtime == crunch_bool]
Или полностью:
Результаты очень интересные.

Пока игра близка, команды не прибегают к экстремальным стратегиям (например, набирают больше трех указателей). Но явно труднее получить желаемые броски в зонах 1, 4 и 5. Возможно, нам следует посмотреть на относительные проценты от общего среднего:

Очевидно, что дополнительные усилия защиты вынуждают команды делать больше выстрелов из неэффективных точек (2, 3, 6 и 7) и меньше - из хороших точек (1, 4 и 5). Хотя это может быть просто шум в данных, тот факт, что уменьшение происходило из зон, которые, как известно, были желательными для съемки, заставляет меня усомниться в этом.
А если посмотреть на точность выстрела, картина становится еще интереснее.

Точность выстрела за последние 6 минут игры при падении с близкого расстояния через доску перед пересечением на трехочковой линии, чтобы стать более точным на больших расстояниях. Это вызывает недоумение, однако есть несколько возможных объяснений.
Один из них - предвзятость выбора, когда лучшие игроки команды делают эти удары в данный момент. Другой связан с теорией игры, в которой защита, вероятно, в некоторой степени подтолкнет команды к тому, чтобы они делали эти броски с относительно низким процентом (при нормальных обстоятельствах).
Тем не менее, команды, изучающие эти данные, скажут, что использование длинных троек может быть удивительно ценным предложением.
Подводя итоги, давайте рассмотрим данные более подробно, используя диаграммы.

Броски становятся менее точными почти повсюду внутри трехочковой линии, а угловые тройки становятся заметно сложнее, по-видимому, по мере того, как защита усиливает давление.
Хотя мы ожидаем, что данные будут потенциально немного зашумленными, учитывая, что есть только 3886 снимков, а не более 210 000), снижение по всем направлениям отражено здесь.
По этой причине я бы не стал слишком углубляться в разницу в направлениях (пока). Учитывая небольшой размер выборки, различия влево / вправо могут быть просто шумом, а не отражением тенденции.
Заворачивать
Я лично считаю, что, хотя в наши дни относительно легко находить большие наборы данных, они не всегда содержат функции, которые позволяют мне легко разрезать данные и делать сравнения или делать из них выводы.
Вместо этого мне часто приходится создавать новые функции из существующих данных и использовать свою интуицию, чтобы оценить, где может быть подходящая точка отсечения для создания подмножеств данных.
С этой целью я надеюсь, что приведенный выше пример был полезен и для вас, а возможно, также интересен.
Пожалуйста, загрузите данные и код, поиграйте с ними и создайте что-то подобное с вашим собственным набором данных. Я хотел бы услышать о вашем опыте или комментарии!
Если вам понравилось, скажите 👋 / подписывайтесь на twitter или следите за обновлениями. Я также написал эти статьи о баскетбольных данных, которые могут помочь в написании этой статьи, если вы не читали их раньше.