Nice Heatmaps¶
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, Rectangle, Arc
from matplotlib.offsetbox import OffsetImage,AnnotationBbox
from matplotlib import cm
from nba_api.stats.endpoints import shotchartdetail,leaguedashplayerbiostats
import seaborn as sns
from scipy.ndimage.filters import gaussian_filter
from skimage import io
%matplotlib inline
Get Data Using API¶
I'm reading the shot chart detail for the entire 2018-19 season. This might take a minute to complete.
shotdata = shotchartdetail.ShotChartDetail(team_id='0',player_id='0',season_nullable='2018-19',
context_measure_simple='FGM',timeout=60)
shotchart,leagueavergae = shotdata.get_data_frames()
Plot Court Function¶
def court(ax=None, line_color='black', lw=4, outer_lines=False,direction='up',short_three=False):
'''
Plots an NBA court
outer_lines - accepts False or True. Plots the outer side lines of the court.
direction - 'up' or 'down' depending on how you like to view the court
Original function from http://savvastjortjoglou.com/
'''
# If an axes object isn't provided to plot onto, just get current one
if ax is None:
if direction=='up':
ax = plt.gca(xlim = [-30,30],ylim = [43,-7],xticks=[],yticks=[],aspect=1.0)
elif direction=='down':
ax = plt.gca(xlim = [30,-30],ylim = [-7,43],xticks=[],yticks=[],aspect=1.0)
else:
ax = plt.gca()
# Create the various parts of an NBA basketball court
# Create the basketball hoop
# Diameter of a hoop is 1.5
hoop = Circle((0, 0), radius=0.75, linewidth=lw/2, color=line_color, fill=False)
# Create backboard
backboard = Rectangle((-3, -0.75), 6, -0.1, linewidth=lw, color=line_color)
# The paint
# Create the outer box of the paint, width=16ft, height=19ft
outer_box = Rectangle((-8, -5.25), 16, 19, linewidth=lw, color=line_color,
fill=False)
# Create the inner box of the paint, widt=12ft, height=19ft
inner_box = Rectangle((-6, -5.25), 12, 19, linewidth=lw, color=line_color,
fill=False)
# Create free throw top arc
top_free_throw = Arc((0, 13.75), 12, 12, theta1=0, theta2=180,
linewidth=lw, color=line_color, fill=False)
# Create free throw bottom arc
bottom_free_throw = Arc((0, 13.75), 12, 12, theta1=180, theta2=0,
linewidth=lw, color=line_color, linestyle='dashed')
# Restricted Zone, it is an arc with 4ft radius from center of the hoop
restricted = Arc((0, 0), 8, 8, theta1=0, theta2=180, linewidth=lw,
color=line_color)
# Three point line
if not short_three:
corner_three_a = Rectangle((-22, -5.25), 0, np.sqrt(23.75**2-22.0**2)+5.25, linewidth=lw,
color=line_color)
corner_three_b = Rectangle((22, -5.25), 0, np.sqrt(23.75**2-22.0**2)+5.25, linewidth=lw, color=line_color)
# 3pt arc - center of arc will be the hoop, arc is 23'9" away from hoop
three_arc = Arc((0, 0), 47.5, 47.5, theta1=np.arccos(22/23.75)*180/np.pi, theta2=180.0-np.arccos(22/23.75)*180/np.pi, linewidth=lw,
color=line_color)
else:
corner_three_a = Rectangle((-22, -5.25), 0, 5.25, linewidth=lw,
color=line_color)
corner_three_b = Rectangle((22, -5.25), 0, 5.25, linewidth=lw, color=line_color)
# 3pt arc - center of arc will be the hoop, arc is 23'9" away from hoop
three_arc = Arc((0, 0), 44.0, 44.0, theta1=0, theta2=180, linewidth=lw,
color=line_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]
if outer_lines:
# Draw the half court line, baseline and side out bound lines
outer_lines = Rectangle((-25, -5.25), 50, 46.75, linewidth=lw,
color=line_color, fill=False)
center_outer_arc = Arc((0, 41.25), 12, 12, theta1=180, theta2=0,
linewidth=lw, color=line_color)
center_inner_arc = Arc((0, 41.25), 4, 4, theta1=180, theta2=0,
linewidth=lw, color=line_color)
court_elements = court_elements + [outer_lines,center_outer_arc,center_inner_arc]
else:
ax.plot([-25,25],[-5.25,-5.25],linewidth=lw,color=line_color)
# Add the court elements onto the axes
for element in court_elements:
ax.add_patch(element)
# ax.axis('off')
return ax
Player's Picture Function¶
def players_picture(player_id):
url = "https://ak-static.cms.nba.com/wp-content/uploads/headshots/nba/latest/260x190/{}.png".format(player_id)
return io.imread(url)
Choose Player¶
sc = shotchart[shotchart['PLAYER_NAME']=='Stephen Curry'].copy()
sc.head()
The Seaborn Method¶
We can use the Kernel Density Estimator (kde) in seaborn in order to plot our heatmap. In this case we will use a Gaussian Kernel (the default for the seaborn jointplot). With the kde, we will mutiply each shot by a 2D Gaussian and it is going to make our data much smoother.
Two important parameters are the bandwidth (bw) and the number of levels (n_levels):
The bandwidth decides the $\sigma$ of the Gaussian. If you don't set it, seaborn will use the scott method the calculate it. For plotting heatmaps of NBA players I prefer to set the bw manually for 2 reasons - the scott method does not necessarly has the same $\sigma_x$ and $\sigma_y$ which can distort the heatmap and also every heatmap will have a different $\sigma$ which makes it hard to compare players to other players.
The number of levels makes the colormap discrete. This will allow to visualize changes much more clearly.
joint_shot_chart = sns.jointplot(sc.LOC_X/10, sc.LOC_Y/10, stat_func=None,
kind='kde', space=0,bw = 2,n_levels = 20)
joint_shot_chart.fig.set_size_inches(12,11)
ax = joint_shot_chart.ax_joint
#ax.scatter(sc.LOC_X/10, sc.LOC_Y/10,marker = '+',c='w',alpha=0.1)
court(ax=ax)
ax.set_xlim(25.0,-25.0)
ax.set_ylim(-4.75,42.25)
Faster kde¶
The process of multiplying each point by a 2D Gaussian is time consuming (as you probably noticed - the seaborn plot takes a few seconds to display). But since our data is discrete, we can first do a 2D bin and then convolve the 2D data with a Gaussian kernel. Mathmetically, this is equivalent to multiplying each point by a Gaussian, and computationally it works much faster.
There are a few ways to do the convolution with a Gaussian kernel. One way is to use scipy Gaussian filter function.
Let's write a function:
- Do a 2D histogram to get the shots location. The bin size is the same size as the resolution of the shot location data.
- Convolve the 2D histogram results with a Gaussian kernel where sigma is the bandwidth of the kernel (sigma can be chosen by user).
- Create a color map with n discrete levels (n is a user choice).
- Plot NBA court.
- Overlay the results of step 2 with the color map of step 3 on the NBA court.
- Add the player's picture
def shot_heatmap(df,sigma = 1,levels = 20,log=False,player_id=None,zoom=0.5,
pic_loc=(19, 37),ax=None,cmap='Blues'):
'''
This function plots a heatmap based on the shot chart.
input - dataframe with x and y coordinates.
optional - log (default false) plots heatmap in log scale.
player (default true) adds player's picture and name if true
sigma - the sigma of the Gaussian kernel. In feet (default=1)
'''
n,_,_ = np.histogram2d( 0.1*df['LOC_X'].values, 0.1*df['LOC_Y'].values,bins = [500,500],range = [[-25,25],[-5.25,44.75]])
N = gaussian_filter(n,10.0*sigma)
if log:
N = np.log(N)
ccmap = cm.get_cmap(cmap,levels)
ccmap.set_bad('white')
if ax is None:
ax = plt.gca(xlim = [30,-30],ylim = [-7,43],xticks=[],yticks=[],aspect=1.0)
court(ax,outer_lines=True,line_color='black',lw=2.0,direction='down')
ax.axis('off')
ax.imshow(np.rot90(N),cmap=ccmap,extent=[-25.0, 25.0, -5.25, 44.75])
try:
if player_id:
pic = players_picture(player_id)
pic2 = OffsetImage(pic, zoom=0.5)
ab = AnnotationBbox(pic2, pic_loc, xycoords='data', frameon=False)
ax.add_artist(ab)
#ax.imshow(pic,extent=[15,25,30,37.8261])
except:
print("cannot load player's picture")
return ax
plt.figure(figsize=(12,12))
ax = shot_heatmap(sc,sigma=2,levels = 20,player_id=201939)
ax.set_xlim(25.0,-25.0);
ax.set_ylim(-5.25,41.50);
We can also add the individual shots
plt.figure(figsize=(12,12))
ax = shot_heatmap(sc,sigma=2,levels = 20,player_id=201939)
ax.scatter(0.1*sc['LOC_X'].values, 0.1*sc['LOC_Y'].values,s = 10,marker='o',alpha=0.1,color='black')
ax.set_xlim(25.0,-25.0);
ax.set_ylim(-5.25,41.50);
And we can easily do this for other players:
player_name = 'James Harden'
df = shotchart[shotchart['PLAYER_NAME']==player_name].copy()
player_id = df.iloc[0,3]
plt.figure(figsize=(12,12))
ax = shot_heatmap(df,sigma=2,levels = 20,player_id=player_id,log=False)
ax.set_xlim(25.0,-25.0);
ax.set_ylim(-5.25,41.50);
plt.savefig('Heatmaps.png',bbox_inches='tight')
Comments
comments powered by Disqus