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:
# 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
)
avg
Using our modified plot_team
module, we can easily generate a team shotchart from this data:
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
:
# 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)
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.
# 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)
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:
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
)
NBAapi.plot_team.grantland_shotchart(hou_shotchart_2014, avg_2014, img=hou_img)
NBAapi.plot_team.grantland_shotchart(hou_shotchart_2015, avg_2015, img=hou_img)
NBAapi.plot_team.grantland_shotchart(hou_shotchart_2016, avg_2016, img=hou_img)
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¶
- Well, new to me at least.
- NBAapi was updated using 2to3-3.6