Danblog

Medical Resident, Software Developer, Basketball Fan

NBA Team Shot Charts


Introduction

I noticed something new1 while poking around stats.nba.com (the NBA's official stats portal). While looking at a team's field goal attempts for the season, a Hex Map now appears alongside the shot plot and shot zones figures (see an example here). I've loved these Kirk Goldsberry-style hex maps since they first appeared on Grantland. In fact, I don't think there's anything more responsible for my interest in NBA analytics than seeing how much information this design could pack into such a clean and beautiful graphic. There are several good blog posts out there about creating similar graphics using the NBA Stats API and Python, most notably from Savvas Tjortjoglou and Eyal Shafran. I'd recommend reading Eyal Shafran's post if you're unfamiliar with these figures.

In this post, I'll demonstrate how to re-create these Hex Maps for entire team's rather than individual players with Python. I'll also flip the traditional approach and create Hex Maps that compare a team's allowed Opponent Field Goal percentage to league average for each shot location, giving some insight into a team's particular defensive strengths and weaknesses (ex: protecting the rim, defending the three point line, etc.)

Modifying NBAapi

To generate the Hex Map shot charts, I'll use matplotlib. Thankfully, most of the work required for generating a basketball court and overlying a hexbin has already been done by the two individuals I link to above. Eyal Shafran's NBAapi Python package provides functions for creating Hex Maps for individual players. I've forked NBAapi and updated it to Python 32. Additionally, I've added another module, plot_team.py, which is just a modified version of plot.py for working with entire teams rather than individual players.

We can use the NBAapi package to hit the API's shotchartdetail endpoint, which yields field goal percentages for each "shot zone" on the court for both the specified team or player as well as the league average:

In [1]:
# forked NBAapi available here: https://github.com/danielwelch/NBAapi
import NBAapi

%matplotlib inline

HOU_ID = 1610612745
ORL_ID = 1610612753

hou_shotchart, avg = NBAapi.shotchart.shotchartdetail(
    season="2017-18", teamid=HOU_ID
)

orl_shotchart, avg = NBAapi.shotchart.shotchartdetail(
    season="2017-18", teamid=ORL_ID
)
In [2]:
avg
Out[2]:
GRID_TYPE SHOT_ZONE_BASIC SHOT_ZONE_AREA SHOT_ZONE_RANGE FGA FGM FG_PCT
0 League Averages Above the Break 3 Back Court(BC) Back Court Shot 33 1 0.030
1 League Averages Above the Break 3 Center(C) 24+ ft. 9850 3523 0.358
2 League Averages Above the Break 3 Left Side Center(LC) 24+ ft. 13300 4775 0.359
3 League Averages Above the Break 3 Right Side Center(RC) 24+ ft. 12514 4400 0.352
4 League Averages Backcourt Back Court(BC) Back Court Shot 350 4 0.011
5 League Averages In The Paint (Non-RA) Center(C) 8-16 ft. 7393 3081 0.417
6 League Averages In The Paint (Non-RA) Center(C) Less Than 8 ft. 11282 4325 0.383
7 League Averages In The Paint (Non-RA) Left Side(L) 8-16 ft. 1232 494 0.401
8 League Averages In The Paint (Non-RA) Right Side(R) 8-16 ft. 1228 487 0.397
9 League Averages Left Corner 3 Left Side(L) 24+ ft. 5285 2040 0.386
10 League Averages Mid-Range Center(C) 8-16 ft. 1582 671 0.424
11 League Averages Mid-Range Center(C) 16-24 ft. 4779 2010 0.421
12 League Averages Mid-Range Left Side Center(LC) 16-24 ft. 4348 1759 0.405
13 League Averages Mid-Range Left Side(L) 16-24 ft. 1679 675 0.402
14 League Averages Mid-Range Left Side(L) 8-16 ft. 4229 1656 0.392
15 League Averages Mid-Range Right Side Center(RC) 16-24 ft. 4548 1789 0.393
16 League Averages Mid-Range Right Side(R) 16-24 ft. 1510 584 0.387
17 League Averages Mid-Range Right Side(R) 8-16 ft. 4010 1647 0.411
18 League Averages Restricted Area Center(C) Less Than 8 ft. 43397 27335 0.630
19 League Averages Right Corner 3 Right Side(R) 24+ ft. 4763 1923 0.404

Using our modified plot_team module, we can easily generate a team shotchart from this data:

In [3]:
NBAapi.plot_team.grantland_shotchart(hou_shotchart, avg)

We can add the team's logo to these charts using the included images in the NBAapi data folder, again with some slight modification in plot_team.py:

In [4]:
# utility function for getting team logo from eyalshafran/NBAapi data folder
from nba_py import team
import pkg_resources
import os
import imageio
import numpy as np

def logo_path(teamid):
    # return path to logo for given teamid
    df = team.TeamList().info()[:30]
    team_map = dict(zip(df.TEAM_ID, df.ABBREVIATION))
    
    DATA_PATH = pkg_resources.resource_filename('NBAapi', 'data/')
    FILENAME = team_map[teamid] + '.png'
    return np.flip(imageio.imread(os.path.join(DATA_PATH, FILENAME)), 1)  # flip the image
    

hou_img = logo_path(HOU_ID)
orl_img = logo_path(ORL_ID)


NBAapi.plot_team.grantland_shotchart(hou_shotchart, avg, img=hou_img)
In [5]:
NBAapi.plot_team.grantland_shotchart(orl_shotchart, avg, img=orl_img)

Opponent Shooting

Lastly, we can flip this concept around and look at the volume and FG% of shots allowed by a team by location. The shotchartdetail endpoint of the NBA Stats API allows you to specifiy an opponent team ID, returning all shot zone data against a specific team.

In [6]:
# We're throwing away the league average value because with this input, the endpoint will
# return the league average against the specified team, which is redundant information.
# We can reuse the average we got above, which is league-wide.

opp_hou_shotchart, _ = NBAapi.shotchart.shotchartdetail(
    season="2017-18", oppteamid=HOU_ID
)

opp_orl_shotchart, _ = NBAapi.shotchart.shotchartdetail(
    season="2017-18", oppteamid=ORL_ID
)

NBAapi.plot_team.grantland_shotchart(opp_hou_shotchart, avg, img=hou_img)  
In [7]:
NBAapi.plot_team.grantland_shotchart(opp_orl_shotchart, avg, img=orl_img)

This can provide some insights into the type of shots a team allows more or less frequently than league average (which is valuable information due to the difference in expected points, on average, per shot location), as well as how well a team defends a particular part of the floor relative to the rest of the league.

Oh the times, they are a changin'

For a couple of years, it was hard to have a conversation about the NBA without someone bringing up the death of the midrange jumper and the trend toward increasing shot volumes from three point range and the paint. We all know the well-founded ideas behind this trend. In the collective NBA consciousness, no team has embodied these ideas more than the Houston Rockets. With these shot charts, we can take a 30,000-foot view at how Houston's shot selection has changed over the years:

In [8]:
hou_shotchart_2014, avg_2014 = NBAapi.shotchart.shotchartdetail(
    season="2014-15", teamid=HOU_ID
)

hou_shotchart_2015, avg_2015 = NBAapi.shotchart.shotchartdetail(
    season="2015-16", teamid=HOU_ID
)
hou_shotchart_2016, avg_2016 = NBAapi.shotchart.shotchartdetail(
    season="2016-17", teamid=HOU_ID
)
hou_shotchart_2017, avg_2017 = NBAapi.shotchart.shotchartdetail(
    season="2017-18", teamid=HOU_ID
)
In [9]:
NBAapi.plot_team.grantland_shotchart(hou_shotchart_2014, avg_2014, img=hou_img)
In [10]:
NBAapi.plot_team.grantland_shotchart(hou_shotchart_2015, avg_2015, img=hou_img)
In [11]:
NBAapi.plot_team.grantland_shotchart(hou_shotchart_2016, avg_2016, img=hou_img)
In [12]:
NBAapi.plot_team.grantland_shotchart(hou_shotchart_2017, avg_2017, img=hou_img)

Among other insights, we can easily see that the volume of midrange shots taken by the Rockets has decreased significantly over the last four years. Additionally, the data for this team reinforces that deeper three point shots are becoming more common, as distant hexbins with little to no volume in the 2014 shotchart are showing much larger volumes by the current season.

The shotchartdetail endpoint allows us to specify date ranges in greater detail (ex. months), which could allow us to analyze changes in shot selection and accuracy within a single season.

Conclusion

After some slight modifications, we can use the NBAapi Python package and the stats.nba.com API to generate shot charts for entire teams rather than individual players. Each of these team graphics provides information about both the volume of shots and the accuracy of those shots from a particular part of the floor. We can use this same endpoint from the API to generate figures for opponent field goal percentage, and we can even compare how a team's field goal attempts (or allowed field goal attempts) have changed over time.

Footnotes

  1. Well, new to me at least.
  2. NBAapi was updated using 2to3-3.6