Chapter 15: The Sports Analytics case study¶
In [1]:
import pandas as pd
import seaborn as sns
import json
from urllib import request
Get the data¶
In [2]:
shots_url = 'https://www.murach.com/python_analysis/shots.json'
shots = request.urlretrieve(shots_url, filename='shots.json')
In [3]:
with open('shots.json') as jsonData:
shots = json.load(jsonData)
shots.keys()
Out[3]:
dict_keys(['resource', 'parameters', 'resultSets'])
In [4]:
columnHeaders = shots['resultSets'][0]['headers']
columnHeaders = [x.lower() for x in columnHeaders]
columnHeaders
Out[4]:
['grid_type', 'game_id', 'game_event_id', 'player_id', 'player_name', 'team_id', 'team_name', 'period', 'minutes_remaining', 'seconds_remaining', 'event_type', 'action_type', 'shot_type', 'shot_zone_basic', 'shot_zone_area', 'shot_zone_range', 'shot_distance', 'loc_x', 'loc_y', 'shot_attempted_flag', 'shot_made_flag', 'game_date', 'htm', 'vtm']
In [5]:
rows = shots['resultSets'][0]['rowSet']
In [6]:
df = pd.DataFrame(data=rows, columns=columnHeaders)
df.head(4)
Out[6]:
| grid_type | game_id | game_event_id | player_id | player_name | team_id | team_name | period | minutes_remaining | seconds_remaining | ... | shot_zone_area | shot_zone_range | shot_distance | loc_x | loc_y | shot_attempted_flag | shot_made_flag | game_date | htm | vtm | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Shot Chart Detail | 0020900015 | 4 | 201939 | Stephen Curry | 1610612744 | Golden State Warriors | 1 | 11 | 25 | ... | Right Side Center(RC) | 24+ ft. | 26 | 99 | 249 | 1 | 0 | 20091028 | GSW | HOU |
| 1 | Shot Chart Detail | 0020900015 | 17 | 201939 | Stephen Curry | 1610612744 | Golden State Warriors | 1 | 9 | 31 | ... | Left Side Center(LC) | 16-24 ft. | 18 | -122 | 145 | 1 | 1 | 20091028 | GSW | HOU |
| 2 | Shot Chart Detail | 0020900015 | 53 | 201939 | Stephen Curry | 1610612744 | Golden State Warriors | 1 | 6 | 2 | ... | Center(C) | 8-16 ft. | 14 | -60 | 129 | 1 | 0 | 20091028 | GSW | HOU |
| 3 | Shot Chart Detail | 0020900015 | 141 | 201939 | Stephen Curry | 1610612744 | Golden State Warriors | 2 | 9 | 49 | ... | Left Side(L) | 16-24 ft. | 19 | -172 | 82 | 1 | 0 | 20091028 | GSW | HOU |
4 rows × 24 columns
Clean the data¶
In [7]:
df.period.unique()
Out[7]:
array([1, 2, 4, 3, 5, 6])
In [8]:
df = df.query('period < 5.0')
In [9]:
df.nunique(dropna=False)
Out[9]:
grid_type 1 game_id 692 game_event_id 692 player_id 1 player_name 1 team_id 1 team_name 1 period 4 minutes_remaining 12 seconds_remaining 60 event_type 2 action_type 51 shot_type 2 shot_zone_basic 7 shot_zone_area 6 shot_zone_range 5 shot_distance 71 loc_x 489 loc_y 437 shot_attempted_flag 1 shot_made_flag 2 game_date 692 htm 32 vtm 32 dtype: int64
In [10]:
shots = df.drop(columns=['grid_type','game_event_id','team_id',
'team_name','player_id','shot_zone_range','shot_zone_basic',
'shot_zone_area','event_type','action_type', 'minutes_remaining',
'seconds_remaining', 'shot_distance','player_name','period','htm',
'vtm','shot_attempted_flag'])
In [11]:
shots['game_date'] = pd.to_datetime(shots['game_date'])
In [12]:
shots.info()
<class 'pandas.core.frame.DataFrame'> Index: 11753 entries, 0 to 11845 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 game_id 11753 non-null object 1 shot_type 11753 non-null object 2 loc_x 11753 non-null int64 3 loc_y 11753 non-null int64 4 shot_made_flag 11753 non-null int64 5 game_date 11753 non-null datetime64[ns] dtypes: datetime64[ns](1), int64(3), object(2) memory usage: 642.7+ KB
Prepare the data¶
In [13]:
shots.head(3)
Out[13]:
| game_id | shot_type | loc_x | loc_y | shot_made_flag | game_date | |
|---|---|---|---|---|---|---|
| 0 | 0020900015 | 3PT Field Goal | 99 | 249 | 0 | 2009-10-28 |
| 1 | 0020900015 | 2PT Field Goal | -122 | 145 | 1 | 2009-10-28 |
| 2 | 0020900015 | 2PT Field Goal | -60 | 129 | 0 | 2009-10-28 |
In [14]:
shots = shots.set_index('game_id')
In [15]:
def get_season(row):
if row.game_date.month > 6:
season = f'{row.game_date.year}-{row.game_date.year + 1}'
else:
season = f'{row.game_date.year - 1}-{row.game_date.year}'
return season
shots['season'] = shots.apply(get_season, axis=1)
shots.head()
Out[15]:
| shot_type | loc_x | loc_y | shot_made_flag | game_date | season | |
|---|---|---|---|---|---|---|
| game_id | ||||||
| 0020900015 | 3PT Field Goal | 99 | 249 | 0 | 2009-10-28 | 2009-2010 |
| 0020900015 | 2PT Field Goal | -122 | 145 | 1 | 2009-10-28 | 2009-2010 |
| 0020900015 | 2PT Field Goal | -60 | 129 | 0 | 2009-10-28 | 2009-2010 |
| 0020900015 | 2PT Field Goal | -172 | 82 | 0 | 2009-10-28 | 2009-2010 |
| 0020900015 | 2PT Field Goal | -68 | 148 | 0 | 2009-10-28 | 2009-2010 |
In [16]:
shots['shot_result'] = shots.shot_made_flag.replace({0:'Missed', 1:'Made'})
shots.head()
Out[16]:
| shot_type | loc_x | loc_y | shot_made_flag | game_date | season | shot_result | |
|---|---|---|---|---|---|---|---|
| game_id | |||||||
| 0020900015 | 3PT Field Goal | 99 | 249 | 0 | 2009-10-28 | 2009-2010 | Missed |
| 0020900015 | 2PT Field Goal | -122 | 145 | 1 | 2009-10-28 | 2009-2010 | Made |
| 0020900015 | 2PT Field Goal | -60 | 129 | 0 | 2009-10-28 | 2009-2010 | Missed |
| 0020900015 | 2PT Field Goal | -172 | 82 | 0 | 2009-10-28 | 2009-2010 | Missed |
| 0020900015 | 2PT Field Goal | -68 | 148 | 0 | 2009-10-28 | 2009-2010 | Missed |
In [17]:
shots['shot_type'].unique()
Out[17]:
array(['3PT Field Goal', '2PT Field Goal'], dtype=object)
In [18]:
shots['points_made'] = shots.apply(lambda x: 0 if x.shot_result == 'Missed' else
(3 if x.shot_type == '3PT Field Goal' else 2), axis=1)
shots.head()
Out[18]:
| shot_type | loc_x | loc_y | shot_made_flag | game_date | season | shot_result | points_made | |
|---|---|---|---|---|---|---|---|---|
| game_id | ||||||||
| 0020900015 | 3PT Field Goal | 99 | 249 | 0 | 2009-10-28 | 2009-2010 | Missed | 0 |
| 0020900015 | 2PT Field Goal | -122 | 145 | 1 | 2009-10-28 | 2009-2010 | Made | 2 |
| 0020900015 | 2PT Field Goal | -60 | 129 | 0 | 2009-10-28 | 2009-2010 | Missed | 0 |
| 0020900015 | 2PT Field Goal | -172 | 82 | 0 | 2009-10-28 | 2009-2010 | Missed | 0 |
| 0020900015 | 2PT Field Goal | -68 | 148 | 0 | 2009-10-28 | 2009-2010 | Missed | 0 |
In [19]:
shots['points_made_game'] = shots.groupby('game_id').points_made.transform('sum')
In [20]:
shots['shots_attempted'] = shots.groupby('game_id').shot_made_flag.transform('count')
In [21]:
shots['shots_made'] = shots.groupby('game_id').shot_made_flag.transform('sum')
In [22]:
shots[['shot_type','points_made','points_made_game','shots_attempted','shots_made']]
Out[22]:
| shot_type | points_made | points_made_game | shots_attempted | shots_made | |
|---|---|---|---|---|---|
| game_id | |||||
| 0020900015 | 3PT Field Goal | 0 | 14 | 12 | 7 |
| 0020900015 | 2PT Field Goal | 2 | 14 | 12 | 7 |
| 0020900015 | 2PT Field Goal | 0 | 14 | 12 | 7 |
| 0020900015 | 2PT Field Goal | 0 | 14 | 12 | 7 |
| 0020900015 | 2PT Field Goal | 0 | 14 | 12 | 7 |
| ... | ... | ... | ... | ... | ... |
| 0021801205 | 3PT Field Goal | 3 | 25 | 20 | 11 |
| 0021801215 | 2PT Field Goal | 0 | 5 | 4 | 2 |
| 0021801215 | 3PT Field Goal | 3 | 5 | 4 | 2 |
| 0021801215 | 3PT Field Goal | 0 | 5 | 4 | 2 |
| 0021801215 | 2PT Field Goal | 2 | 5 | 4 | 2 |
11753 rows × 5 columns
Plot the summary data¶
In [23]:
shotsSeason = shots[['season','game_date','points_made_game','shots_made',
'shots_attempted']].drop_duplicates()
In [24]:
sns.catplot(data=shotsSeason, kind='box', x='season', y='points_made_game',
aspect=2.5, hue='season', palette='deep', legend=False)
Out[24]:
<seaborn.axisgrid.FacetGrid at 0x13694c890>
In [25]:
shotsSeason.head()
Out[25]:
| season | game_date | points_made_game | shots_made | shots_attempted | |
|---|---|---|---|---|---|
| game_id | |||||
| 0020900015 | 2009-2010 | 2009-10-28 | 14 | 7 | 12 |
| 0020900030 | 2009-2010 | 2009-10-30 | 12 | 5 | 9 |
| 0020900069 | 2009-2010 | 2009-11-04 | 7 | 3 | 6 |
| 0020900082 | 2009-2010 | 2009-11-06 | 2 | 1 | 5 |
| 0020900096 | 2009-2010 | 2009-11-08 | 9 | 4 | 8 |
In [26]:
shotsSeasonAvg = shotsSeason.groupby('season').mean().reset_index()
In [27]:
shotsSeasonAvg.plot(
x='season', y=['points_made_game','shots_made','shots_attempted'],
color={'points_made_game':'red','shots_made':'blue','shots_attempted':'green'},
figsize=(8,5), ylim=(0,30))
Out[27]:
<Axes: xlabel='season'>
Plot the shots for two games¶
In [28]:
# two games with a lot of shots
gameIDs = ['0021800923','0021800642']
g = sns.relplot(data=shots.query('game_id in @gameIDs'), kind='scatter',
x='loc_x', y='loc_y', hue='shot_result', col='game_id')
In [29]:
# SOURCE: http://savvastjortjoglou.com/nba-shot-sharts.html
from matplotlib.patches import Circle, Rectangle, Arc
def draw_court(ax=None, color='black', lw=2, outer_lines=False):
# If an axes object isn't provided to plot onto, just get current one
if ax is None:
ax = plt.gca()
# Create the various parts of an NBA basketball court
# Create the basketball hoop
# Diameter of a hoop is 18" so it has a radius of 9", which is a value
# 7.5 in our coordinate system
hoop = Circle((0, 0), radius=7.5, linewidth=lw, color=color, fill=False)
# Create backboard
backboard = Rectangle((-30, -7.5), 60, -1, linewidth=lw, color=color)
# The paint
# Create the outer box 0f the paint, width=16ft, height=19ft
outer_box = Rectangle((-80, -47.5), 160, 190, linewidth=lw, color=color,
fill=False)
# Create the inner box of the paint, widt=12ft, height=19ft
inner_box = Rectangle((-60, -47.5), 120, 190, linewidth=lw, color=color,
fill=False)
# Create free throw top arc
top_free_throw = Arc((0, 142.5), 120, 120, theta1=0, theta2=180,
linewidth=lw, color=color, fill=False)
# Create free throw bottom arc
bottom_free_throw = Arc((0, 142.5), 120, 120, theta1=180, theta2=0,
linewidth=lw, color=color, linestyle='dashed')
# Restricted Zone, it is an arc with 4ft radius from center of the hoop
restricted = Arc((0, 0), 80, 80, theta1=0, theta2=180, linewidth=lw,
color=color)
# Three point line
# Create the side 3pt lines, they are 14ft long before they begin to arc
corner_three_a = Rectangle((-220, -47.5), 0, 140, linewidth=lw,
color=color)
corner_three_b = Rectangle((220, -47.5), 0, 140, linewidth=lw, color=color)
# 3pt arc - center of arc will be the hoop, arc is 23'9" away from hoop
# I just played around with the theta values until they lined up with the
# threes
three_arc = Arc((0, 0), 475, 475, theta1=22, theta2=158, linewidth=lw,
color=color)
# Center Court
center_outer_arc = Arc((0, 422.5), 120, 120, theta1=180, theta2=0,
linewidth=lw, color=color)
center_inner_arc = Arc((0, 422.5), 40, 40, theta1=180, theta2=0,
linewidth=lw, color=color)
# List of the court elements to be plotted onto the axes
court_elements = [hoop, backboard, outer_box, inner_box, top_free_throw,
bottom_free_throw, restricted, corner_three_a,
corner_three_b, three_arc, center_outer_arc,
center_inner_arc]
if outer_lines:
# Draw the half court line, baseline and side out bound lines
outer_lines = Rectangle((-250, -47.5), 500, 470, linewidth=lw,
color=color, fill=False)
court_elements.append(outer_lines)
# Add the court elements onto the axes
for element in court_elements:
ax.add_patch(element)
return ax
In [30]:
g = sns.relplot(data=shots.query('game_id in @gameIDs'), kind='scatter',
x='loc_x', y='loc_y', hue='shot_result', col='game_id', s=50)
for i, ax in enumerate(g.axes.flat):
ax.set_title('shots for game ' + gameIDs[i])
ax = draw_court(ax, outer_lines=True)
ax.set_xlim(-300,300)
ax.set_ylim(-100, 500)
Plot shot data for two seasons¶
In [31]:
colors = ['#FF0B04','#4374B3'] # blue and red
sns.set_palette(sns.color_palette(colors))
seasons = ['2009-2010','2018-2019']
g = sns.relplot(data=shots.query('season in @seasons'), kind='scatter',
x='loc_x', y='loc_y', hue='shot_result', col='season', col_wrap=1)
for ax in g.axes.flat:
ax = draw_court(ax, outer_lines=True)
ax.set_xlim(-300, 300)
ax.set_ylim(-100, 500)
Plot shot density for one season¶
In [32]:
colors = ['#4374B3','#FF0B04'] # red and blue
sns.set_palette(sns.color_palette(colors))
g = sns.displot(data=shots.query('season == "2015-2016"'), kind='kde', legend=False,
x='loc_x', y='loc_y', col='shot_result', hue='shot_result', col_wrap=1)
for ax in g.axes.flat:
ax = draw_court(ax, outer_lines=True)
ax.set_xlim(-300, 300)
ax.set_ylim(-100, 500)
Plot shot density for two seasons¶
In [33]:
colors = ['#FF0B04','#4374B3'] # blue and red
sns.set_palette(sns.color_palette(colors))
seasons = ['2009-2010','2015-2016']
g = sns.displot(data=shots.query('season in @seasons'), kind='kde',
x='loc_x', y='loc_y', row='shot_result', col='season',
hue='shot_result', legend=False)
for ax in g.axes.flat:
ax = draw_court(ax, outer_lines=True)
ax.set_xlim(-300, 300)
ax.set_ylim(-100, 500)