Nice Heatmaps

In [2]:
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.

In [3]:
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

In [4]:
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

In [5]:
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

In [6]:
sc = shotchart[shotchart['PLAYER_NAME']=='Stephen Curry'].copy()
sc.head()
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
184 Shot Chart Detail 0021800002 10 201939 Stephen Curry 1610612744 Golden State Warriors 1 11 31 ... Right Side Center(RC) 24+ ft. 24 226 90 1 1 20181016 GSW OKC
197 Shot Chart Detail 0021800002 58 201939 Stephen Curry 1610612744 Golden State Warriors 1 7 57 ... Center(C) Less Than 8 ft. 6 -2 63 1 1 20181016 GSW OKC
215 Shot Chart Detail 0021800002 131 201939 Stephen Curry 1610612744 Golden State Warriors 1 3 46 ... Center(C) Less Than 8 ft. 2 5 29 1 1 20181016 GSW OKC
217 Shot Chart Detail 0021800002 141 201939 Stephen Curry 1610612744 Golden State Warriors 1 2 58 ... Center(C) Less Than 8 ft. 1 -6 18 1 0 20181016 GSW OKC
219 Shot Chart Detail 0021800002 145 201939 Stephen Curry 1610612744 Golden State Warriors 1 2 49 ... Left Side(L) 24+ ft. 22 -228 8 1 1 20181016 GSW OKC

5 rows × 24 columns

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):

  1. 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.

  2. The number of levels makes the colormap discrete. This will allow to visualize changes much more clearly.

In [7]:
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)
Out[7]:
(-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:

  1. Do a 2D histogram to get the shots location. The bin size is the same size as the resolution of the shot location data.
  2. Convolve the 2D histogram results with a Gaussian kernel where sigma is the bandwidth of the kernel (sigma can be chosen by user).
  3. Create a color map with n discrete levels (n is a user choice).
  4. Plot NBA court.
  5. Overlay the results of step 2 with the color map of step 3 on the NBA court.
  6. Add the player's picture
In [8]:
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
In [9]:
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

In [10]:
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:

In [11]:
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