tl;dr

All the shots and FT attempts in one animation made with NBA spatio-temporal data (maintained by neilmj) and paper.js.

The data is from Golden State Warriors vs Denver Nuggets on January 13th 2016.

Workflow

I took the following simple two steps.

Data cooking with Python

Animation with Paper.js

Data cooking with Python

The final goal of this step is to generate a JSON file for Paper.js.

Concretely I like to have Let’s first get the data.

import json import pandas as pd import os import numpy as np from collections import defaultdict from itertools import compress import urllib os . chdir ( 'PATH/TO/YOUR/WORKINGDIRECTORY' ) tid = '1610612744' # Team ID for GSW gid = '0021500583' # Game ID of this game # Let's get the data datalink = ( "https://raw.githubusercontent.com/neilmj/BasketballData/" "master/2016.NBA.Raw.SportVU.Game.Logs/01.13.2016.GSW.at.DEN.7z" ) # You can either download the data and unzip the above URL manually or do the # following os . system ( "curl " + datalink + " -o " + os . getcwd () + "/zipped_data" ) os . system ( "7za x " + os . getcwd () + "/zipped_data" ) # This should output 0021500583.json in your working directory with open ( '{gid}.json' . format ( gid = gid )) as data_file : data = json . load ( data_file ) # Load this json # You can explore the data here if you like # Following code and we get player lists for both home # and visitors home = [ data [ "events" ][ 0 ][ "home" ][ "players" ][ i ][ "playerid" ] for i in xrange ( len ( data [ "events" ][ 0 ][ "home" ][ "players" ]))] visitors = [ data [ "events" ][ 0 ][ "visitor" ][ "players" ][ i ][ "playerid" ] for i in xrange ( len ( data [ "events" ][ 0 ][ "visitor" ][ "players" ]))] pids = home + visitors # This is going to be a list of all the player IDs

In the next snippet, we are going to call a play by play API from stats.nba.com/.

Play by play data contains how each event ends up like 2PT attemps, rebound, turnover or substitution.

We are going to use this information to get only field goal and free throw attempts.

os . system ( 'curl "http://stats.nba.com/stats/playbyplayv2?' 'EndPeriod=0&' 'EndRange=0&' 'GameID={gid}&' 'RangeType=0&' 'Season=2015-16&' 'SeasonType=Season&' 'StartPeriod=0&' 'StartRange=0" > {cwd}/pbp_{gid}.json' . format ( cwd = os . getcwd (), gid = gid )) # Download json from the API pbp = pd . DataFrame () with open ( "pbp_{gid}.json" . format ( gid = gid )) as json_file : parsed = json . load ( json_file )[ 'resultSets' ][ 0 ] pbp = pbp . append ( pd . DataFrame ( parsed [ 'rowSet' ], columns = parsed [ 'headers' ])) # Conver only the necessary part into DataFrame shot_events = pbp [ pbp [ "EVENTMSGTYPE" ] . isin ([ 1 , 2 , 3 ])][ "EVENTNUM" ] . values # EVENTMSGTYPE is how an event ends up # 1: FG made # 2: FG missed # 3: FT raw_json = list () for event in xrange ( len ( data [ 'events' ])): #for each event if event in shot_events : # if event was any attempt at a rim for num in xrange ( 0 , len ( data [ 'events' ][ event ][ 'moments' ]), 1 ): # for each record of data. apparently every 0.1-0.2 second lstlsts = data [ 'events' ][ event ][ 'moments' ][ num ][ 5 ] # this block contains spatio-temporal data for all the players on # the court tmplst = list () for pid in [ - 1 ] + pids : # for each player and ball (pids -1) if any ( pid in lst for lst in lstlsts ) == True : # for each player indx = [ pid in lst for lst in lstlsts ] # get an array index of a player rw = list ( compress ( lstlsts , indx ))[ 0 ] # and get the row itself tmplst . append ( rw [ 2 ]) # X coordinate tmplst . append ( rw [ 3 ]) # Y coordinate else : tmplst . append ( None ) tmplst . append ( None ) # Add None if the player is on bench raw_json . append ( tmplst ) json_df = pd . DataFrame ( raw_json ) json_df . fillna ( 0 , inplace = True ) # get all tthe resting players to (0, 0) json_df . drop_duplicates ( inplace = True )

json_df has X, Y coordinates of all the players per observation.

Each row is a time (recorded every 0.1 - 0.2 sec).

Column zero is a X coordinate of player 1 (pids[0]), column one is a Y coordinate of the same player, column two is a X-coordinate of player 2 (pids [1]) and so on.

The last trick needed is to convert Python DataFrame to a right for Paper.js.

There are more than one way to do this but we are going to make a nba_data.js and define JavaScript variables in there.

json_str = json_df . drop_duplicates () . values . tolist () json_str = "var DATA = " + str ( json_str ) + "

" json_str += "var count1 = " + str ( len ( home )) + "

" json_str += "var count2 = " + str ( len ( pids ) + 1 ) + "

" json_str += "var pids = " + str ( pids ) with open ( 'nba_data.js' , 'w' ) as f : f . write ( json_str )

Animation with Paper.js

Now we got the data and now we are going to make animation in JavaScript.

The below is a simple html skelton.

Put this in the project folder so this can find nba_data.js.

I downloaded Paper.js by bower install paper so the location might be different from bower_components/paper/dist/paper-full.js depending on how you download the library.

<!DOCTYPE html> <html> <head> <!-- Load the saved JavaScript variables --> <script src= "nba_data.js" ></script> <!-- Load paper.js --> <script type= "text/javascript" src= "bower_components/paper/dist/paper-full.js" ></script> <!-- Make animation --> <script type= "text/paperscript" canvas= "myCanvas" > <!-- WE ARE GOING TO FILL IN HERE --> </script> </head> <body> <canvas id= "myCanvas" style= "background:black" resize= True ></canvas> </body> </html>

As you can see in the script tag above, Paper.js needs a special type=”text/paperscript” and canvas=ID.

The content of a canvas is shown in html <canvas id = ID> part.

The last part is to put the following in the above WE ARE GOING TO FILL IN HERE.

// We start with making 11 circle objects with Path.Circle, which are // essentially going to be wrapped up into Symbol objects // Ball comes first var path0 = new Path . Circle ({ center : [ 0 , 0 ], radius : 3 , fillColor : 'brown' , strokeColor : 'brown' }); var symbol0 = new Symbol ( path0 ); // White and blue color home court jersey for Denver Nuggets var path = new Path . Circle ({ center : [ 0 , 0 ], radius : 3 , fillColor : 'white' , strokeColor : 'skyblue' }); var symbol = new Symbol ( path ); // Blue and yellow away jersey for Golden State Warriors var path2 = new Path . Circle ({ center : [ 0 , 0 ], radius : 3 , fillColor : 'blue' , strokeColor : 'yellow' }); var symbol2 = new Symbol ( path2 ); // Assign center and scale for the 11 objects var center = Point . random (); var placedSymbol0 = symbol0 . place ( center ); placedSymbol0 . scale ( 1 ); for ( var i = 1 ; i <= count1 ; i ++ ) { var center = Point . random (); var placedSymbol = symbol . place ( center ); placedSymbol . scale ( 1.5 ); } for ( var i = count1 + 1 ; i <= count2 ; i ++ ) { var center = Point . random (); var placedSymbol2 = symbol2 . place ( center ); placedSymbol2 . scale ( 1.5 ); } // Everything in onFrame will be in animation // When this function is defined, it is called up to 60 times a second by Paper.js // The number of count is obtained by event.count so using this to assign // new position to each object function onFrame ( event ) { project . activeLayer . children [ 0 ]. position = [ DATA [ event . count ][ 0 ] * 3 , DATA [ event . count ][ 1 ] * 2.5 ]; for ( var i = 1 ; i <= count1 ; i ++ ) { var item = project . activeLayer . children [ i ]; item . position = [ DATA [ event . count ][( i * 2 )] * 3 , DATA [ event . count ][( i * 2 ) + 1 ] * 2.5 ]; } for ( var i = count1 + 1 ; i <= count2 ; i ++ ) { var item = project . activeLayer . children [ i ]; item . position = [ DATA [ event . count ][( i * 2 )] * 3 , DATA [ event . count ][( i * 2 ) + 1 ] * 2.5 ]; } }

Showcase

The final output in html is a smooth one game shot highlight animatioin too big to be fit in this post. so here are some GIF extractions from the original JavaScript animation.

Conclusion

Though I prefer a black background with circle objects moving around, you can draw a basketball court or replace circiles with player photos (you know, there’s also API for photos).

The full code is available here

Please enable JavaScript to view the comments powered by Disqus.

Disqus