import%20marimo%0A%0A__generated_with%20%3D%20%220.9.4%22%0Aapp%20%3D%20marimo.App(width%3D%22medium%22%2C%20auto_download%3D%5B%22html%22%5D)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20__()%3A%0A%20%20%20%20import%20marimo%0A%0A%20%20%20%20marimo.md(%22%22%22%0A%20%20%20%20%23%20NHL%20Player%20%26%20Puck%20Tracking%0A%0A%20%20%20%20Hurray!%20We%20finally%20have%20NHL%20tracking%20data.%20Long%20have%20we%20waited%20since%20the%20rumblings%20of%20SMT%20working%20with%20the%20league.%20%0A%0A%20%20%20%20Goals%20now%20have%20%22simulations%22%20on%20the%20game%20page.%20For%20this%20example%20Notebook%2C%20I'll%20be%20working%20with%20this%20magnificant%20goal%20from%20Cole%20Caufield.%20Here's%20%5Bthe%20game%5D(https%3A%2F%2Fwww.nhl.com%2Fgamecenter%2Fmtl-vs-tor%2F2024%2F10%2F09%2F2024020006).%20There%20is%20JSON%20attached%20to%20that%20particular%20event.%20I%20believe%20we%20can%20assume%20that%20there%20are%20many%20of%20these%20JSON%20files%2C%20but%20they%20are%20only%20being%20made%20public%20for%20goals%20and%20when%20the%20data%20doesn't%20require%20some%20cleanup.%20%0A%0A%20%20%20%20Here's%20the%20code%20I%20have%20so%20far%20to%20make%20sense%20of%20it%2C%20I've%20added%20some%20quick%20comments%20as%20well%20so%20that%20you%20can%20understand%20what%20I'm%20doing%20with%20the%20data.%20%0A%0A%20%20%20%20Please%20reach%20out%20on%20%5BTwitter%5D(https%3A%2F%2Ftwitter.com%2Fdannypage)%20and%20%5BBluesky%5D(https%3A%2F%2Fbsky.app%2Fprofile%2Fdanny.page)%20if%20you%20have%20any%20questions.%20I'd%20also%20like%20to%20see%20what%20you%20all%20are%20doing%20with%20the%20data%20too!%20%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%20(marimo%2C)%0A%0A%0A%40app.cell%0Adef%20__()%3A%0A%20%20%20%20import%20requests%0A%20%20%20%20import%20pandas%20as%20pd%0A%20%20%20%20import%20numpy%20as%20np%0A%20%20%20%20import%20math%0A%0A%20%20%20%20X%20%3D%202400%20%23%202400%20inches%20-%20length%20of%20the%20rink%0A%20%20%20%20Y%20%3D%201020%20%23%201020%20inches%20-%20width%20of%20the%20rink%0A%0A%20%20%20%20%23%20This%20is%20one%20of%20the%20example%20goals%20in%20the%20datafeed.%20%0A%20%20%20%20%23%20I%20believe%20the%20full%20PBP%20has%20a%20%22pptReplayUrl%22%20for%20the%20link%2C%20so%20anyone%20parsing%20all%20the%20data%20can%20hit%20this.%0A%20%20%20%20response%20%3D%20requests.get('https%3A%2F%2Fwsr.nhle.com%2Fsprites%2F20242025%2F2024020006%2Fev223.json')%0A%20%20%20%20events%20%3D%20response.json()%0A%0A%20%20%20%20%23%20Let's%20flatten%20the%20data%20a%20bit%2C%20and%20also%20fix%20the%20Timestamp%0A%20%20%20%20%23%20It's%20setup%20as%20f%22%7Bepoch_seconds%7D%7Btenths_of_second%7D%22%20which%20would%20be%20confusing%20for%20most%20parsers.%0A%20%20%20%20frames%20%3D%20%5B%5D%0A%20%20%20%20for%20e%20in%20events%3A%0A%20%20%20%20%20%20%20%20objects%20%3D%20%5Bx%20for%20x%20in%20e%5B'onIce'%5D.values()%5D%0A%20%20%20%20%20%20%20%20for%20o%20in%20objects%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20o%5B'timestamp'%5D%20%3D%20e%5B'timeStamp'%5D%2F10.0%0A%20%20%20%20%20%20%20%20%20%20%20%20frames.append(o)%0A%0A%20%20%20%20%23%20Add%20the%20list%20of%20dicts%20to%20a%20dataframe%3A%0A%20%20%20%20df%20%3D%20pd.DataFrame(frames)%0A%0A%20%20%20%20%23%20Sort%20by%20ID%20and%20timestamp.%20This%20will%20be%20helpful%20when%20we%20eventually%20save%20this%20data%20to%20Parquet%0A%20%20%20%20%23%20And%20also%20makes%20a%20lot%20of%20the%20%22_delta%22%20variables%20easier%20to%20calculate.%0A%20%20%20%20df%20%3D%20df.sort_values(%5B%22id%22%2C%20%22timestamp%22%5D)%0A%0A%20%20%20%20first_timestamp%20%3D%20df%5Bdf.id%20%3D%3D1%5D%5B'timestamp'%5D.iloc%5B0%5D%0A%0A%20%20%20%20%23%20In%20some%20cases%2C%20a%20player%20may%20not%20appear%20in%20the%20data.%20We%20need%20to%20interpolate%20from%20the%20last%20time%20we%20saw%20them.%0A%20%20%20%20df%5B%22time_delta%22%5D%20%3D%20df.groupby(%5B%22id%22%5D)%5B%22timestamp%22%5D.diff().fillna(0.0)%0A%20%20%20%20df%5B%22x_delta%22%5D%20%3D%20df.groupby(%5B%22id%22%5D)%5B%22x%22%5D.diff().fillna(0.0)%0A%20%20%20%20df%5B%22y_delta%22%5D%20%3D%20df.groupby(%5B%22id%22%5D)%5B%22y%22%5D.diff().fillna(0.0)%0A%20%20%20%20df%5B%22distance%22%5D%20%3D%20np.sqrt(df.x_delta**2%20%2B%20df.y_delta**2)%0A%0A%20%20%20%20%23%20Once%20we%20have%20a%20timedelta%20between%20observations%20and%20a%20distance%20delta%2C%20we%20can%20calculate%20the%20speed.%0A%20%20%20%20df%5B%22speed%22%5D%20%3D%20(df.distance%20%2F%20df.time_delta).fillna(0.0)%0A%0A%20%20%20%20%23%20Also%20perhaps%20the%20angle%20of%20the%20travel%20for%20the%20object%3F%20This%20may%20require%20smoothing.%0A%20%20%20%20df%5B%22angle%22%5D%20%3D%20np.arctan2(df%5B'y_delta'%5D%2C%20df%5B'x_delta'%5D)*180%2Fmath.pi%0A%20%20%20%20df%5B%22angle_delta%22%5D%20%3D%20np.arctan2(np.sin(df.groupby(%5B%22id%22%5D)%5B%22angle%22%5D.diff())%2C%20np.cos(df.groupby(%5B%22id%22%5D)%5B%22angle%22%5D.diff()))%20*%20180%20%2F%20np.pi%0A%20%20%20%20df%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20X%2C%0A%20%20%20%20%20%20%20%20Y%2C%0A%20%20%20%20%20%20%20%20df%2C%0A%20%20%20%20%20%20%20%20e%2C%0A%20%20%20%20%20%20%20%20events%2C%0A%20%20%20%20%20%20%20%20first_timestamp%2C%0A%20%20%20%20%20%20%20%20frames%2C%0A%20%20%20%20%20%20%20%20math%2C%0A%20%20%20%20%20%20%20%20np%2C%0A%20%20%20%20%20%20%20%20o%2C%0A%20%20%20%20%20%20%20%20objects%2C%0A%20%20%20%20%20%20%20%20pd%2C%0A%20%20%20%20%20%20%20%20requests%2C%0A%20%20%20%20%20%20%20%20response%2C%0A%20%20%20%20)%0A%0A%0A%40app.cell%0Adef%20__(df)%3A%0A%20%20%20%20%23%20This%20is%20just%20the%20Puck's%20data.%20This%20will%20be%20the%20primary%20focus%20for%20now.%20%0A%20%20%20%20df%5Bdf.id%20%3D%3D%201%5D%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20__(df%2C%20first_timestamp%2C%20np)%3A%0A%20%20%20%20import%20matplotlib.pyplot%20as%20plt%0A%0A%20%20%20%20%23%20Calculate%20atan2%20values%0A%20%20%20%20atan2_values%20%3D%20np.arctan2(df%5Bdf.id%20%3D%3D%201%5D.y%2C%20df%5Bdf.id%20%3D%3D%201%5D.x)%0A%0A%20%20%20%20%23%20Plot%20the%20graph%20of%20angles%0A%20%20%20%20plt.plot(df%5Bdf.id%20%3D%3D%201%5D.timestamp%20-%20first_timestamp%2C%20df%5Bdf.id%3D%3D1%5D.angle_delta)%0A%20%20%20%20plt.xlabel('Time')%0A%20%20%20%20plt.ylabel('Degree%20Delta')%0A%20%20%20%20plt.title('Degree%20Delta%20Over%20Time')%0A%20%20%20%20plt.grid(True)%0A%20%20%20%20plt.show()%0A%20%20%20%20return%20atan2_values%2C%20plt%0A%0A%0A%40app.cell%0Adef%20__(df%2C%20first_timestamp%2C%20plt)%3A%0A%20%20%20%20from%20scipy.signal%20import%20find_peaks%0A%20%20%20%20peaks%2C%20heights%20%3D%20find_peaks(df%5Bdf.id%3D%3D1%5D.speed%2C%20height%3D0)%0A%0A%20%20%20%20fixed_peaks%20%3D%20%5Bx%2F10.0%20for%20x%20in%20peaks%5D%0A%0A%20%20%20%20peaks_valley%2C%20valley_heights%20%3D%20find_peaks(df%5Bdf.id%3D%3D1%5D.speed*-1%2C%20height%3D-1000)%0A%20%20%20%20fixed_valleys%20%3D%20%5Bx%2F10.0%20for%20x%20in%20peaks_valley%5D%0A%20%20%20%20print(peaks_valley)%0A%0A%20%20%20%20%23%20Plot%20the%20graph%0A%20%20%20%20plt.plot(df%5Bdf.id%20%3D%3D%201%5D.timestamp%20-%20first_timestamp%2C%20df%5Bdf.id%3D%3D1%5D.speed)%0A%20%20%20%20plt.plot(fixed_peaks%2C%20heights%5B'peak_heights'%5D%2C%20%22x%22)%0A%20%20%20%20plt.plot(fixed_valleys%2C%20valley_heights%5B'peak_heights'%5D*-1%2C%20%22x%22)%0A%20%20%20%20plt.xlabel('Time')%0A%20%20%20%20plt.ylabel('speed')%0A%20%20%20%20plt.grid(True)%0A%20%20%20%20plt.show()%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20find_peaks%2C%0A%20%20%20%20%20%20%20%20fixed_peaks%2C%0A%20%20%20%20%20%20%20%20fixed_valleys%2C%0A%20%20%20%20%20%20%20%20heights%2C%0A%20%20%20%20%20%20%20%20peaks%2C%0A%20%20%20%20%20%20%20%20peaks_valley%2C%0A%20%20%20%20%20%20%20%20valley_heights%2C%0A%20%20%20%20)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20__(marimo)%3A%0A%20%20%20%20marimo.md(%22%22%22After%20tracking%20the%20peaks%20and%20valleys%20of%20the%20puck%20speed%2C%20I%20think%20we%20can%20use%20this%20to%20isolate%20where%20the%20puck%20changes%20course.%20For%20example%2C%20at%20about%208%20seconds%20into%20the%20file%2C%20that's%20when%20we%20can%20see%20Cole's%20shot.%20It%20hits%20the%20net%20at%208.1%20and%20then%20immediately%20slows%20down%20to%20almost%200%20inches%20per%20second.%0A%0A%20%20%20%20Now%20that%20we%20know%20where%20the%20shot%20takes%20place%2C%20we%20can%20see%20where%20the%20players%20are%20at%20that%20moment.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20__(df%2C%20first_timestamp%2C%20fixed_peaks%2C%20fixed_valleys)%3A%0A%20%20%20%20%23%20find%20closest%20player%20at%20each%20peak%0A%20%20%20%20from%20scipy.spatial.distance%20import%20cdist%0A%0A%20%20%20%20def%20closest_node(node%2C%20nodes)%3A%0A%20%20%20%20%20%20%20%20return%20cdist(node%2C%20nodes).argmin()%0A%0A%20%20%20%20print(first_timestamp)%0A%0A%20%20%20%20for%20p%20in%20fixed_peaks%3A%0A%20%20%20%20%20%20%20%20ts%20%3D%20round(first_timestamp%20%2B%20p%2C1)%0A%0A%20%20%20%20%20%20%20%20puck%20%3D%20df%5B(df.timestamp%20%3D%3D%20ts)%20%26%20(df.id%20%3D%3D%201)%5D%5B%5B%22x%22%2C%20%22y%22%5D%5D%0A%20%20%20%20%20%20%20%20players%20%3D%20df%5B(df.timestamp%20%3D%3D%20ts)%20%26%20(df.id%20!%3D%201)%5D%5B%5B%22x%22%2C%20%22y%22%5D%5D%0A%20%20%20%20%20%20%20%20index%20%3D%20closest_node(%20puck%2C%20players)%0A%20%20%20%20%20%20%20%20player%20%3D%20df%5B(df.timestamp%20%3D%3D%20ts)%20%26%20(df.id%20!%3D%201)%5D.iloc%5Bindex%5D.to_dict()%0A%20%20%20%20%20%20%20%20%23print(f%22%7Bp%7D%3A%20%7Bplayer%5B'sweaterNumber'%5D%7D%20of%20%7Bplayer%5B'teamAbbrev'%5D%7D%22)%0A%0A%0A%20%20%20%20for%20v%20in%20fixed_valleys%3A%0A%20%20%20%20%20%20%20%20ts%20%3D%20round(first_timestamp%20%2B%20v%2C1)%0A%0A%20%20%20%20%20%20%20%20puck%20%3D%20df%5B(df.timestamp%20%3D%3D%20ts)%20%26%20(df.id%20%3D%3D%201)%5D%5B%5B%22x%22%2C%20%22y%22%5D%5D%0A%20%20%20%20%20%20%20%20players%20%3D%20df%5B(df.timestamp%20%3D%3D%20ts)%20%26%20(df.id%20!%3D%201)%5D%5B%5B%22x%22%2C%20%22y%22%5D%5D%0A%20%20%20%20%20%20%20%20index%20%3D%20closest_node(%20puck%2C%20players)%0A%20%20%20%20%20%20%20%20player%20%3D%20df%5B(df.timestamp%20%3D%3D%20ts)%20%26%20(df.id%20!%3D%201)%5D.iloc%5Bindex%5D.to_dict()%0A%20%20%20%20%20%20%20%20%23print(f%22%7Bv%7D%3A%20%7Bplayer%5B'sweaterNumber'%5D%7D%20of%20%7Bplayer%5B'teamAbbrev'%5D%7D%22)%0A%0A%20%20%20%20for%20a%20in%20sorted(fixed_valleys%20%2B%20fixed_peaks)%3A%0A%20%20%20%20%20%20%20%20ts%20%3D%20round(first_timestamp%20%2B%20a%2C1)%0A%0A%20%20%20%20%20%20%20%20puck%20%3D%20df%5B(df.timestamp%20%3D%3D%20ts)%20%26%20(df.id%20%3D%3D%201)%5D%5B%5B%22x%22%2C%20%22y%22%5D%5D%0A%20%20%20%20%20%20%20%20players%20%3D%20df%5B(df.timestamp%20%3D%3D%20ts)%20%26%20(df.id%20!%3D%201)%5D%5B%5B%22x%22%2C%20%22y%22%5D%5D%0A%20%20%20%20%20%20%20%20index%20%3D%20closest_node(%20puck%2C%20players)%0A%20%20%20%20%20%20%20%20player%20%3D%20df%5B(df.timestamp%20%3D%3D%20ts)%20%26%20(df.id%20!%3D%201)%5D.iloc%5Bindex%5D.to_dict()%0A%20%20%20%20%20%20%20%20print(f%22%7Ba%7D%3A%20%7Bplayer%5B'sweaterNumber'%5D%7D%20of%20%7Bplayer%5B'teamAbbrev'%5D%7D%22)%0A%0A%0A%20%20%20%20%23closest%20%3D%20df%5Bdf%5B'id'%5D%20!%3D%20target_id%5D.nsmallest(1%2C%20'distance')%0A%20%20%20%20return%20a%2C%20cdist%2C%20closest_node%2C%20index%2C%20p%2C%20player%2C%20players%2C%20puck%2C%20ts%2C%20v%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20__(marimo)%3A%0A%20%20%20%20marimo.md(%22%22%22%0A%20%20%20%20Stopping%20here%20for%20now%20-%20but%20I%20feel%20very%20confident%20that%20just%20by%20looking%20for%20valleys%20%26%20peaks%20in%20the%20speed%2C%20we%20can%20identify%20the%20players%20directly%20involved%20in%20the%20play.%20For%20example%3A%0A%0A%20%20%20%20%20-%2034%20(TOR)%20and%2014%20(MTL)%20are%20engaged%20in%20the%20faceoff%20to%20begin.%0A%20%20%20%20%20-%208%20(MTL)%20takes%20the%20puck%20and%20dribbles%20the%20puck%2C%20looking%20for%20a%20pass%0A%20%20%20%20%20-%2034%20(TOR)%20bears%20down%20on%208%20(MTL)%20and%208%20passes%20to%2077%20(MTL)%0A%20%20%20%20%20-%2077%20(MTL)%20dribbles%20as%20well%2C%20finds%20the%20pass%20to%2020%20(MTL)%2C%20reaching%20top%20speed%20near%202%20(TOR)%0A%20%20%20%20%20-%2020%20(MTL)%20one-time%20passes%20to%2013%20(MTL).%20On%20the%20way%20it%20reaches%20top%20speed%20near%2041%20(TOR)%0A%20%20%20%20%20-%2013%20(MTL)%20has%20a%20one-time%20snapshot%20into%20the%20net.%0A%0A%20%20%20%20The%20rest%20of%20the%20peaks%2Fvalleys%20are%20the%20puck%20settling%20into%20the%20net%20near%20the%20Toronto%20defender%20and%20goalie.%0A%0A%20%20%20%20I'm%20curious%20how%20this%20would%20do%20with%20a%20puck%20battle%20on%20the%20boards%2C%20but%20for%20most%20cases%2C%20this%20simple%20speed%20check%20might%20be%20enough.%20Will%20have%20to%20try%20on%20another%20goal.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20app.run()%0A
f0e8bf74accccd99ad785f15d54439501d814b178f359352112c98813ca50083