The Geography of Basketball: Mapping NBA Shotcharts in ArcGIS

A guest post by Gregory Brunner

There are a lot of great blog posts out there about techniques to get and plot basketball data using the NBA stats API. Two that I highly recommend are Web Scraping 201: Finding the API and How to Create NBA Shot Charts in R. Here, I want to show how you can use Python to push this data into a Geographic Information System, ArcGIS. From there, you can leverage concepts, tools, and applications that are generally reserved for geography and geographers to make some great visuals from NBA player shot chart data.

Accessing the NBA stats API with Python is pretty straightforward. Here I can get whole season of shots for a player given their player ID and the season.

def get_player_season(player_id, season):
    master_shots=[]
    coords = []
    seasontype="Regular%20Season"
    seasonindicator=0
    nba_call_url='http://stats.nba.com/stats/shotchartdetail?Season=%s&SeasonType=%s&TeamID=0&PlayerID=%s&GameID=&Outcome=&Location=&Month=0&SeasonSegment=&DateFrom=&Dateto=&OpponentTeamID=0&VsConference=&VsDivision=&Position=&RookieYear=&GameSegment=&Period=0&LastNGames=0&ContextMeasure=FGA' % (season,seasontype, player_id)
    plays=urllib2.urlopen(nba_call_url)
    data=json.load(plays)
    for row in data['resultSets'][0]['rowSet']:
        three=0
        if row[12]=='3PT Field Goal':
            three=1
        temp=(row[0], row[1], row[2], row[3], row[4], row[5], row[6], row[7],
                row[8], row[9], row[10],row[11], row[12], row[13], row[14],
                row[15], row[16], row[17],row[18], row[19], row[20], three)
        coord = ([row[17],row[18]])
        coords.append((coord,)+temp)

    return master_shots, coords

Rather than plot the data straight into ggplot (R) or matplotlib (Python) or push the data into our own SQL database, we can store the data in a ArcGIS feature class in a geodatabase. Using arcpy, we can create a feature class and add the fields from the data that we want to store.

def create_feature_class(output_gdb, output_feature_class):
    feature_class = os.path.basename(output_feature_class)
    if not arcpy.Exists(output_gdb):
        arcpy.CreateFileGDB_management(os.path.dirname(output_gdb),os.path.basename(output_gdb))
        arcpy.CreateFeatureclass_management(output_gdb,feature_class,"POINT","#","DISABLED","DISABLED", "PROJCS['WGS_1984_Web_Mercator_Auxiliary_Sphere',GEOGCS['GCS_WGS_1984',DATUM['D_WGS_1984',SPHEROID['WGS_1984',6378137.0,298.257223563]],PRIMEM['Greenwich',0.0],UNIT['Degree',0.0174532925199433]],PROJECTION['Mercator_Auxiliary_Sphere'],PARAMETER['False_Easting',0.0],PARAMETER['False_Northing',0.0],PARAMETER['Central_Meridian',0.0],PARAMETER['Standard_Parallel_1',0.0],PARAMETER['Auxiliary_Sphere_Type',0.0],UNIT['Meter',1.0]]","#","0","0","0")
    if not arcpy.Exists(output_feature_class):
        arcpy.AddField_management(output_feature_class,"GRID_TYPE","TEXT", "", "", 100)
        arcpy.AddField_management(output_feature_class,"GAME_ID","TEXT", "", "", 100)
        arcpy.AddField_management(output_feature_class,"GAME_EVENT_ID","TEXT", "", "", 100)
        arcpy.AddField_management(output_feature_class,"PLAYER_ID","TEXT", "", "", 100)
        arcpy.AddField_management(output_feature_class,"PLAYER_NAME","TEXT", "", "", 100)
        arcpy.AddField_management(output_feature_class,"TEAM_ID","TEXT", "", "", 100)
        arcpy.AddField_management(output_feature_class,"TEAM_NAME","TEXT", "", "", 100)
        arcpy.AddField_management(output_feature_class,"PERIOD","SHORT", "", "", "")
        arcpy.AddField_management(output_feature_class,"MINUTES_REMAINING","SHORT", "", "", "")
        arcpy.AddField_management(output_feature_class,"SECONDS_REMAINING","SHORT", "", "", "")
        arcpy.AddField_management(output_feature_class,"EVENT_TYPE","TEXT", "", "", 100)
        arcpy.AddField_management(output_feature_class,"ACTION_TYPE","TEXT", "", "", 100)
        arcpy.AddField_management(output_feature_class,"SHOT_TYPE","TEXT", "", "", 100)
        arcpy.AddField_management(output_feature_class,"SHOT_ZONE_BASIC","TEXT", "", "", 100)
        arcpy.AddField_management(output_feature_class,"SHOT_ZONE_AREA","TEXT", "", "", 100)
        arcpy.AddField_management(output_feature_class,"SHOT_ZONE_RANGE","TEXT", "", "", 100)
        arcpy.AddField_management(output_feature_class,"SHOT_DISTANCE","SHORT", "", "", "")
        arcpy.AddField_management(output_feature_class,"LOC_X","DOUBLE", "", "", "")
        arcpy.AddField_management(output_feature_class,"LOC_Y","DOUBLE", "", "", "")
        arcpy.AddField_management(output_feature_class,"SHOT_ATTEMPTED_FLAG","SHORT", "", "", "")
        arcpy.AddField_management(output_feature_class,"SHOT_MADE_FLAG","SHORT", "", "", "")
        arcpy.AddField_management(output_feature_class,"THREE","SHORT", "", "", "")

Once the data is in Python and I have a feature class created, all that’s left to do is insert the data into the feature class with an arcpy InsertCursor.

def populate_feature_class(rowValues, output_feature_class):
    c = arcpy.da.InsertCursor(output_feature_class,fc_fields)
    for row in rowValues:
        c.insertRow(row)
    del c

At this point, if we just wanted to work in ArcMap, we’d be done. We don’t want to do that. We want to share this data as a hosted feature service in ArcGIS Online so that we can use the functionality of ArcGIS Online. Here’s what Russell Westbrook’s 2014-2015 season shot chart looks like as a webmap.


View larger map

I got the OKC Thunder court image from Zach Lowe’s NBA Court Design Power Rankings and georeferenced is to the data using:

# Replace a layer/table view name with a path to a dataset (which can be a layer file) or create the layer/table view within the script
# The following inputs are layers or table views: "okc.jpg"
arcpy.Warp_management(in_raster="okc.jpg", source_control_points="'920.296 -92.874';'920.296 -526.033';'514.595 -526.033';'514.595 -92.874'", target_control_points="'250 -40';'-250 -40';'-250 390';'250 390'", out_raster="C:/PROJECTS/R&D/NBA/OKC_Court_Scaled_39.tif", transformation_type="POLYORDER1", resampling_type="BILINEAR")

I am a few pixels off in the y-direction, so I’ll have to play around with this more to get the image to align better with the data. If you want to do this for other basketball court images, this code probably needs to be modified a little bit depending on the dimensions of the image.

Now we can use other ArcGIS Online capabilities to give the shot chart a heat map effect.


View larger map

What’s really cool is that once you have a webmap of the shot chart data, you can ‘create a web application.’ I chose to use the summary viewer to visualize the shot chart data because it dynamically calculates the shooting percentage and also allows the capability to filter on a selected attribute. Here I filter on SHOT_TYPE; however, you could choose to filter on SHOT_DISTANCE, ACTION_TYPE, PERIOD, or another attribute. Click in the search window that says “All” and you can filter the data on Russell Westbrook’s shot type.


View Larger App

This is really just scratching the surface of what you can do with this data in ArcGIS. You can try using the analytics, geoprocessing services, and other app templates once you get the data into ArcGIS Online. If you need help getting started, you can find my source code at my Github page.