Mapping NBA Player Movement in 2D and 3D

Having developed methods to get the NBA player shotchart data and player movement data into ArcGIS, I wanted to explore different ways to visualize the player movement data with webmaps, ArcGIS application templates, and 3D web scenes. I also wanted to look at an entire game and find ways to identify where on the court players spend the most time and what lanes or routes players are more likely to travel across the court. For this post, I’m going to describe some approaches to visualize an entire game of movement data with common GIS techniques and tools and use some really cool GIS visualization techniques to display the data and compare player movement maps.

For this post, I looked at the movement data for the entire game between the Oklahoma City Thunder and Detroit Pistons from November 27, 2015. I aligned the data so that the northern end of the basketball court is the Thunder’s offensive end and the southern end of the court is the Piston’s. I filtered the data to remove duplicate timestamps for each player on the court. This has the effect of filtering out moments where game action is occurring but the clock is not moving (for example, foul shots). This is what I wanted to do because I was interested in where players spend time on the court when the game is in progress.

My first thought was to turn player movement points into player movement lines. For every unique event number in unique_event_list, I converted the point features for that event into line features using the PLAYER_ID as the line field. Then, I updated the TEAM_ID, QUARTER, and START_TIME for each line feature to allow me to filter on player name and time. Here is the function I wrote to create the line features.

def create_line_features(in_fc, gdb, unique_event_list, dictionary):
    for event in unique_event_list:
        event_lines = os.path.join(gdb, 'event_'+str(event))
        if arcpy.Exists(event_lines) == False:
            print('Creating lines for event ' + str(event))
            layer = 'in_memory\\event_lyr_' + str(event)
            query = '"EVENT_ID" = ' + str(event)
            arcpy.MakeFeatureLayer_management(in_fc,layer,query,"","")
            arcpy.PointsToLine_management(layer, event_lines, "PLAYER_ID", "GAME_TIME", "NO_CLOSE")
            arcpy.AddField_management(os.path.join(gdb, 'event_'+str(event)),"TEAM_ID","LONG", "", "", "")
            arcpy.AddField_management(os.path.join(gdb, 'event_'+str(event)),"QUARTER","SHORT", "", "", "")
            arcpy.AddField_management(os.path.join(gdb, 'event_'+str(event)), "START_TIME", "TEXT", "", "", 30)
            #Get Start time of lines
            with arcpy.da.SearchCursor(layer, ('GAME_TIME', 'QUARTER')) as cursor:
                first_time = cursor.next()
            #Add start time of lines to line features
            with arcpy.da.UpdateCursor(os.path.join(gdb, 'event_'+str(event)), ('START_TIME', 'PLAYER_ID','TEAM_ID', 'QUARTER')) as cursor:
                for row in cursor:
                    row[0] = first_time[0]
                    row[2] = dictionary[row[1]]
                    row[3] = first_time[1]
                    cursor.updateRow(row)

What does a single event look like as lines?

For single event, I’ve isolated the basketball (orange line), Kevin Durant (blue line), and Ersan Ilyasova (red line). During this event at about 2 minutes into the game, Kevin Durant gets 2-points after driving by Ersan Ilyasova.

When I look at this webmap, it makes me think that ArcGIS and webmaps could be really powerful tools for coaches to do X’s and O’s and diagram plays.

What does this look like for an entire game?

I concatenated all the events into a single feature class. The geodatabase I created above (gdb) contains a set of line features for every unique event and I appended every feature class into an empty features class (output_line_fc) and then deleted the individual event feature classes.

def append_lines(gdb, output_line_fc):
    arcpy.env.workspace = gdb
    fc_list = arcpy.ListFeatureClasses('*',"Polyline")
    arcpy.CreateFeatureclass_management(gdb, os.path.basename(output_line_fc), "POLYLINE", fc_list[0],"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]];-20037700 -30241100 10000;-100000 10000;-100000 10000;0.001;0.001;0.001;IsHighPrecision","#","0","0","0")
    arcpy.Append_management(fc_list, output_line_fc, 'NO_TEST', '', '')
    for fc in fc_list:
        arcpy.Delete_management(fc)

This is what the entire game looks like.

The red lines are the Detroit Pistons, the blue lines are the Oklahoma City Thunder, and the orange lines are the basketball. It’s a real mess! However, what you notice is that at the south end of the basketball court, the red lines cover a wider area and at the north end of the court, the blue lines cover a wider area. The south end of the court is the Piston’s offensive end and the north side is the Thunder’s.

It gets really interesting when you start to isolate the player movement lines for individual players. For example, this is Ersan Ilyasova’s game movement map.

Notice that there are some lines that trace around the 3-point arch on the Piston’s offensive end.

Here is Andre Drummond’s movement map.

Andre plays center for the Pistons. His movement is confined to the center of the basketball court and on the defensive end, he stays within the free throw lane. If you open the webmap, you should be able to toggle between some of the other players on the Pistons and Thunder and also see the movement lines for the basketball.

How do you compare two players’ maps and what could that comparison tell you?

I really quickly created a player movement map comparison using the ArcGIS swipe template. Here I compare Kevin Durant’s and Marcus Morris’ player movement charts. From looking at different maps, I kind of inferred that Marcus and Kevin were assigned to guard each other.

View Larger App

Still, I didn’t think that this was the most clear comparison, so I converted the player movement data to rasters that represent the amount of time each player spends at a given cell. These rasters I’ll refer to as frequency maps because they correspond to how frequent a player visits a given location on the court. I did this for every player in the game.

def create_point_density_raster(in_points, gdb, player_list, player_dictionary):
    for player in player_list:
        query = '"PLAYER_ID" = ' + str(player)
        ras_name = 'player_movement_rasters_'+player_dictionary[player]
        print('Creating ' + str(ras_name))
        ras_name.replace(' ', '_')
        ras_name = re.sub('[^0-9a-zA-Z]+', '_', ras_name)
        out_ras = os.path.join(gdb,ras_name)
        if arcpy.Exists(out_ras) == True:
            player_lyr = 'in_memory\\'+str(player)+'movement_map'
            print('Making feature layer.')
            arcpy.MakeFeatureLayer_management(in_points, player_lyr, query, '#', '#')
            print('Getting point density raster '+ ras_name)
            arcpy.gp.PointDensity_sa(player_lyr, "NONE", out_ras, 2.5, "Circle 10 MAP", "SQUARE_KILOMETERS")

That same player movement comparison between Kevin Durant and Marcus Morris is a little bit clearer when looking at the data for these two players as frequency maps.

View Larger App

I was going to leave that as the only comparison app, but then I found the Compare Analysis template. With this template, I decided to compare the frequency maps of the Thunder’s starting five. Check it out.

View Larger App

It’s cool to see where each player spends the most time on the court and how that compares to their teammates. I think it’d be really cool if I could quantify the entropy of player movement. Who covers more ground? Who tends to be more stationary on the court? Perhaps a topic for another day.

What do these maps look like in 3D?

I explored (and am still exploring!) visualizing player movement as a 3D map with ArcScene and CityEngine web scenes. I created a 3D scene in ArcScene using the frequency maps. I extruded the frequency maps and put the starting five for both the Thunder and the Pistons into a group layer in ArcScene. I saved the scene document (sxd) and published it as a web scene.

sxd = 'C:/NBA/PlayerMovementScene.sxd'
webscene = 'C:/NBA/PlayerComparisons.3ws'
arcpy.ExportTo3DWebScene_3d(sxd, webscene)

Then I uploaded it to my ArcGIS Online account. Take a look.

Definitely Explore in full viewer for the full effect. In addition to viewing an individual frequency map you can compare player frequency maps.

3D Player Movement Comparison

It’s a pretty fun way to visualize player movement.

Again, I feel like I’m just scratching the surface with what is possible here. The code and data are shared on github if you’re interested. If you have any ideas or questions, leave a comment!

See Also