import%20marimo%0A%0A__generated_with%20%3D%20%220.19.4%22%0Aapp%20%3D%20marimo.App(width%3D%22medium%22)%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20import%20marimo%20as%20mo%0A%20%20%20%20from%20nba_api.stats.endpoints%20import%20ScoreboardV3%2C%20PlayByPlayV3%0A%20%20%20%20import%20pandas%20as%20pd%0A%20%20%20%20import%20seaborn%20as%20sns%0A%20%20%20%20import%20matplotlib.pyplot%20as%20plt%0A%20%20%20%20import%20re%0A%20%20%20%20return%20PlayByPlayV3%2C%20pd%2C%20plt%2C%20re%2C%20sns%0A%0A%0A%40app.cell%0Adef%20_(PlayByPlayV3)%3A%0A%20%20%20%20game_id%20%3D%20'0022500753'%0A%20%20%20%20pbp%20%3D%20PlayByPlayV3(game_id%3Dgame_id)%0A%20%20%20%20pbp_df%20%3D%20pbp.get_data_frames()%5B0%5D%0A%20%20%20%20pbp_df%0A%20%20%20%20return%20(pbp_df%2C)%0A%0A%0A%40app.cell%0Adef%20_(pbp_df%2C%20plt%2C%20re%2C%20sns)%3A%0A%20%20%20%20def%20get_ref(text)%3A%0A%20%20%20%20%20%20%20%20%23%20This%20regex%20looks%20for%20names%20inside%20the%20last%20set%20of%20parentheses%0A%20%20%20%20%20%20%20%20match%20%3D%20re.search(r'%5C((%5Cw%5C.%5Cw%2B)%5C)%24'%2C%20str(text).strip())%0A%20%20%20%20%20%20%20%20return%20match.group(1)%20if%20match%20else%20%22Unknown%22%0A%0A%20%20%20%20df_fouls%20%3D%20pbp_df%5Bpbp_df%5B'description'%5D.str.contains(r'%5C(%5Cw%5C.%5Cw%2B%5C)'%2C%20na%3DFalse)%5D.copy()%0A%20%20%20%20df_fouls%5B'Referee'%5D%20%3D%20df_fouls%5B'description'%5D.apply(get_ref)%0A%20%20%20%20counts%20%3D%20df_fouls.groupby(%5B'period'%2C%20'teamTricode'%2C%20'Referee'%5D).size().reset_index(name%3D'Whistles')%0A%0A%20%20%20%20sns.set_theme(style%3D%22ticks%22)%0A%20%20%20%20g%20%3D%20sns.catplot(%0A%20%20%20%20%20%20%20%20data%3Dcounts%2C%20%0A%20%20%20%20%20%20%20%20x%3D'period'%2C%20%0A%20%20%20%20%20%20%20%20y%3D'Whistles'%2C%20%0A%20%20%20%20%20%20%20%20hue%3D'Referee'%2C%20%0A%20%20%20%20%20%20%20%20col%3D'teamTricode'%2C%0A%20%20%20%20%20%20%20%20kind%3D'bar'%2C%20%0A%20%20%20%20%20%20%20%20palette%3D'Set2'%2C%0A%20%20%20%20%20%20%20%20edgecolor%3D'black'%2C%0A%20%20%20%20%20%20%20%20linewidth%3D1%2C%0A%20%20%20%20%20%20%20%20height%3D5%2C%20%0A%20%20%20%20%20%20%20%20width%3D0.6%2C%0A%20%20%20%20%20%20%20%20aspect%3D1.2%0A%20%20%20%20)%0A%0A%20%20%20%20for%20ax%20in%20g.axes.flat%3A%0A%20%20%20%20%20%20%20%20ax.yaxis.grid(True%2C%20color%3D'lightgrey'%2C%20linestyle%3D'-'%2C%20zorder%3D0)%0A%20%20%20%20%20%20%20%20ax.set_axisbelow(True)%0A%0A%20%20%20%20g.set_axis_labels(%22Quarter%22%2C%20%22Number%20of%20Whistles%22)%0A%20%20%20%20g.set_titles(%22%7Bcol_name%7D%20Whistle%20Distribution%22)%0A%20%20%20%20g.fig.suptitle('Foul%20Calls%20by%20Official%20and%20Team%20per%20Quarter'%2C%20y%3D1.05)%0A%0A%20%20%20%20plt.show()%0A%20%20%20%20return%20(get_ref%2C)%0A%0A%0A%40app.cell%0Adef%20_(pbp_df%2C%20pd%2C%20plt%2C%20sns)%3A%0A%20%20%20%20%23%20Actions%20that%20are%20indicative%20of%20driving%20for%20a%20shot%0A%20%20%20%20aggressive_actions%20%3D%20%5B%0A%20%20%20%20%20%20%20%20'Running%20Layup%20Shot'%2C%20'Driving%20Floating%20Jump%20Shot'%2C%20'Running%20Dunk%20Shot'%2C%20%0A%20%20%20%20%20%20%20%20'Layup%20Shot'%2C%20'Cutting%20Layup%20Shot'%2C%20'Driving%20Hook%20Shot'%2C%20%0A%20%20%20%20%20%20%20%20'Cutting%20Finger%20Roll%20Layup%20Shot'%2C%20'Driving%20Finger%20Roll%20Layup%20Shot'%2C%0A%20%20%20%20%20%20%20%20'Alley%20Oop%20Dunk%20Shot'%2C%20'Driving%20Bank%20Hook%20Shot'%2C%20'Running%20Finger%20Roll%20Layup%20Shot'%2C%0A%20%20%20%20%20%20%20%20'Driving%20Layup%20Shot'%2C%20'Driving%20Floating%20Bank%20Jump%20Shot'%2C%20'Putback%20Dunk%20Shot'%2C%0A%20%20%20%20%20%20%20%20'Tip%20Layup%20Shot'%2C%20'Driving%20Reverse%20Layup%20Shot'%2C%20'Running%20Alley%20Oop%20Dunk%20Shot'%2C%0A%20%20%20%20%20%20%20%20'Running%20Alley%20Oop%20Layup%20Shot'%2C%20'Dunk%20Shot'%0A%20%20%20%20%5D%0A%0A%20%20%20%20%23%20Create%20a%20mapping%20to%20find%20the%20opponent%0A%20%20%20%20teams%20%3D%20pbp_df%5B'teamTricode'%5D.unique()%0A%20%20%20%20teams%20%3D%20%5Bt%20for%20t%20in%20teams%20if%20str(t)%20!%3D%20'nan'%20and%20t%20!%3D%20''%5D%0A%20%20%20%20opp_map%20%3D%20%7Bteams%5B0%5D%3A%20teams%5B1%5D%2C%20teams%5B1%5D%3A%20teams%5B0%5D%7D%0A%0A%20%20%20%20attacks_per_q%20%3D%20pbp_df%5Bpbp_df%5B'subType'%5D.isin(aggressive_actions)%5D.groupby(%5B'period'%2C%20'teamTricode'%5D).size().reset_index(name%3D'Count')%0A%20%20%20%20attacks_per_q%5B'Metric'%5D%20%3D%20'Aggressive%20Attacks'%0A%0A%20%20%20%20%23%20Calculate%20Whistles%20DRAWN%20(The%20team%20that%20benefited%20from%20the%20call)%0A%20%20%20%20%23%20We%20find%20rows%20with%20referee%20names%2C%20then%20map%20the%20'teamTricode'%20(fouler)%20to%20their%20opponent%20(fouled)%0A%20%20%20%20df_fouls_updated%20%3D%20pbp_df%5Bpbp_df%5B'description'%5D.str.contains(r'%5C(%5Cw%5C.%5Cw%2B%5C)'%2C%20na%3DFalse)%5D.copy()%0A%20%20%20%20df_fouls_updated%5B'teamTricode'%5D%20%3D%20df_fouls_updated%5B'teamTricode'%5D.map(opp_map)%0A%0A%20%20%20%20whistles_per_q%20%3D%20df_fouls_updated.groupby(%5B'period'%2C%20'teamTricode'%5D).size().reset_index(name%3D'Count')%0A%20%20%20%20whistles_per_q%5B'Metric'%5D%20%3D%20'Whistles%20Drawn'%0A%0A%20%20%20%20%23%205.%20Combine%20and%20Plot%0A%20%20%20%20quarterly_df%20%3D%20pd.concat(%5Battacks_per_q%2C%20whistles_per_q%5D)%0A%0A%20%20%20%20sns.set_theme(style%3D%22ticks%22)%0A%20%20%20%20g_2%20%3D%20sns.catplot(%0A%20%20%20%20%20%20%20%20data%3Dquarterly_df%2C%20%0A%20%20%20%20%20%20%20%20x%3D'period'%2C%20%0A%20%20%20%20%20%20%20%20y%3D'Count'%2C%20%0A%20%20%20%20%20%20%20%20hue%3D'Metric'%2C%20%0A%20%20%20%20%20%20%20%20col%3D'teamTricode'%2C%0A%20%20%20%20%20%20%20%20kind%3D'bar'%2C%20%0A%20%20%20%20%20%20%20%20palette%3D'muted'%2C%0A%20%20%20%20%20%20%20%20edgecolor%3D'black'%2C%0A%20%20%20%20%20%20%20%20linewidth%3D1%2C%0A%20%20%20%20%20%20%20%20height%3D5%2C%20%0A%20%20%20%20%20%20%20%20aspect%3D1.2%2C%0A%20%20%20%20%20%20%20%20sharey%3DTrue%20%0A%20%20%20%20)%0A%0A%20%20%20%20%23%20Visual%20Refinements%0A%20%20%20%20for%20ax_2%20in%20g_2.axes.flat%3A%0A%20%20%20%20%20%20%20%20ax_2.yaxis.grid(True%2C%20color%3D'lightgrey'%2C%20linestyle%3D'-'%2C%20zorder%3D0)%0A%20%20%20%20%20%20%20%20ax_2.set_axisbelow(True)%0A%20%20%20%20%20%20%20%20%23%20Adding%20integer%20labels%20to%20bars%20for%20precise%20comparison%0A%20%20%20%20%20%20%20%20for%20container%20in%20ax_2.containers%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20ax_2.bar_label(container%2C%20padding%3D3)%0A%0A%20%20%20%20g_2.set_axis_labels(%22Quarter%22%2C%20%22Total%20Count%22)%0A%20%20%20%20g_2.set_titles(%22%7Bcol_name%7D%3A%20Offense%20vs.%20Whistles%22)%0A%20%20%20%20g_2.fig.suptitle('Whistles%20Drawn%20Relative%20to%20Rim%20Aggression%20(Per%20Quarter)'%2C%20y%3D1.05)%0A%0A%20%20%20%20plt.show()%0A%20%20%20%20return%20opp_map%2C%20teams%0A%0A%0A%40app.cell%0Adef%20_(get_ref%2C%20opp_map%2C%20pbp_df%2C%20pd%2C%20plt%2C%20sns%2C%20teams)%3A%0A%20%20%20%20%23%20Ensure%20scores%20are%20numeric%20for%20margin%20calculation%0A%20%20%20%20pbp_df%5B'scoreHome'%5D%20%3D%20pd.to_numeric(pbp_df%5B'scoreHome'%5D%2C%20errors%3D'coerce').fillna(0)%0A%20%20%20%20pbp_df%5B'scoreAway'%5D%20%3D%20pd.to_numeric(pbp_df%5B'scoreAway'%5D%2C%20errors%3D'coerce').fillna(0)%0A%20%20%20%20pbp_df%5B'score_margin'%5D%20%3D%20pbp_df%5B'scoreHome'%5D%20-%20pbp_df%5B'scoreAway'%5D%0A%0A%20%20%20%20def%20get_total_seconds(row)%3A%0A%20%20%20%20%20%20%20%20%23%20Standardizing%20ISO%20duration%20to%20total%20seconds%20elapsed%20in%20game%0A%20%20%20%20%20%20%20%20%23%20NBA%20periods%3A%201-4%20are%2012m%20(720s)%0A%20%20%20%20%20%20%20%20duration%20%3D%20pd.to_timedelta(row%5B'clock'%5D).total_seconds()%0A%20%20%20%20%20%20%20%20if%20row%5B'period'%5D%20%3C%3D%204%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20(row%5B'period'%5D%20*%20720)%20-%20duration%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20Handling%20Overtime%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20(4%20*%20720)%20%2B%20((row%5B'period'%5D%20-%204)%20*%20300)%20-%20duration%0A%0A%20%20%20%20%23%20Map%20the%20%22Beneficiary%22%20(the%20team%20that%20got%20the%20whistle)%0A%20%20%20%20%23%20If%20a%20foul%20is%20attributed%20to%20GSW%2C%20DAL%20is%20the%20beneficiary%0A%20%20%20%20teams_2%20%3D%20%5Bt%20for%20t%20in%20pbp_df%5B'teamTricode'%5D.unique()%20if%20pd.notna(t)%20and%20t%20!%3D%20''%5D%0A%20%20%20%20opp_map_2%20%3D%20%7Bteams%5B0%5D%3A%20teams%5B1%5D%2C%20teams%5B1%5D%3A%20teams%5B0%5D%7D%20if%20len(teams)%20%3E%3D%202%20else%20%7B%7D%0A%0A%20%20%20%20pbp_df%5B'seconds_elapsed'%5D%20%3D%20pbp_df.apply(get_total_seconds%2C%20axis%3D1)%0A%0A%20%20%20%20%23%202.%20Identify%20and%20Process%20Whistle%20Events%0A%20%20%20%20df_foul_plot%20%3D%20pbp_df%5Bpbp_df%5B'description'%5D.str.contains(r'%5C(%5Cw%5C.%5Cw%2B%5C)'%2C%20na%3DFalse)%5D.copy()%0A%20%20%20%20df_foul_plot%5B'Beneficiary'%5D%20%3D%20df_foul_plot%5B'teamTricode'%5D.map(opp_map)%0A%0A%20%20%20%20df_foul_plot%5B'Referee'%5D%20%3D%20df_foul_plot%5B'description'%5D.apply(get_ref)%0A%0A%20%20%20%20%23%203.%20Calculate%20Whistle%20Density%20(How%20%22active%22%20are%20the%20refs%3F)%0A%20%20%20%20%23%20We%20mark%20every%20row%20that%20is%20a%20whistle%20and%20use%20a%20rolling%20window%20to%20show%20frequency%0A%20%20%20%20pbp_df%5B'is_whistle'%5D%20%3D%20pbp_df.index.isin(df_foul_plot.index).astype(int)%0A%20%20%20%20%23%20window%3D100%20rows%20roughly%20represents%20a%20few%20minutes%20of%20game%20flow%0A%20%20%20%20pbp_df%5B'whistle_density'%5D%20%3D%20pbp_df%5B'is_whistle'%5D.rolling(window%3D100%2C%20min_periods%3D1).sum()%0A%0A%20%20%20%20%23%204.%20Visualization%0A%20%20%20%20fig%2C%20ax1%20%3D%20plt.subplots(figsize%3D(16%2C%208))%0A%0A%20%20%20%20%23%20General%20Colors%0A%20%20%20%20color_home%20%3D%20'%230072B2'%20%20%20%23%20Royal%20Blue%0A%20%20%20%20color_away%20%3D%20'%23D55E00'%20%20%20%23%20Vermillion%20(High%20contrast)%0A%20%20%20%20color_density%20%3D%20'%23566E7A'%20%23%20Neutral%20Slate%20Gray%0A%0A%20%20%20%20%23%20Background%3A%20Score%20Margin%20(Shaded%20to%20show%20momentum)%0A%20%20%20%20ax1.fill_between(pbp_df%5B'seconds_elapsed'%5D%2C%20pbp_df%5B'score_margin'%5D%2C%200%2C%20%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20where%3D(pbp_df%5B'score_margin'%5D%20%3E%3D%200)%2C%20color%3Dcolor_home%2C%20alpha%3D0.1%2C%20label%3D'LAL%20Lead')%0A%20%20%20%20ax1.fill_between(pbp_df%5B'seconds_elapsed'%5D%2C%20pbp_df%5B'score_margin'%5D%2C%200%2C%20%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20where%3D(pbp_df%5B'score_margin'%5D%20%3C%200)%2C%20color%3Dcolor_away%2C%20alpha%3D0.1%2C%20label%3D'GSW%20Lead')%0A%20%20%20%20ax1.axhline(0%2C%20color%3D'black'%2C%20linestyle%3D'-'%2C%20linewidth%3D0.8%2C%20alpha%3D0.5)%0A%0A%20%20%20%20%23%20Primary%20Axis%3A%20Whistle%20Points%0A%20%20%20%20sns.scatterplot(%0A%20%20%20%20%20%20%20%20data%3Ddf_foul_plot%2C%0A%20%20%20%20%20%20%20%20x%3D'seconds_elapsed'%2C%0A%20%20%20%20%20%20%20%20y%3D'score_margin'%2C%0A%20%20%20%20%20%20%20%20hue%3D'Beneficiary'%2C%0A%20%20%20%20%20%20%20%20style%3D'Referee'%2C%0A%20%20%20%20%20%20%20%20s%3D160%2C%20%0A%20%20%20%20%20%20%20%20%23%20Using%20a%20list%20or%20a%20map%20here%20ensures%20consistency%20regardless%20of%20team%20names%0A%20%20%20%20%20%20%20%20palette%3D%5Bcolor_home%2C%20color_away%5D%2C%20%0A%20%20%20%20%20%20%20%20edgecolor%3D'white'%2C%20%23%20Changed%20to%20white%20for%20better%20%22pop%22%20against%20background%20fills%0A%20%20%20%20%20%20%20%20linewidth%3D1.2%2C%0A%20%20%20%20%20%20%20%20zorder%3D5%2C%0A%20%20%20%20%20%20%20%20ax%3Dax1%0A%20%20%20%20)%0A%0A%20%20%20%20%23%20Secondary%20Axis%3A%20Whistle%20Frequency%20Trend%0A%20%20%20%20ax2%20%3D%20ax1.twinx()%0A%20%20%20%20ax2.plot(pbp_df%5B'seconds_elapsed'%5D%2C%20pbp_df%5B'whistle_density'%5D%2C%20%0A%20%20%20%20%20%20%20%20%20%20%20%20%20color%3Dcolor_density%2C%20linestyle%3D'--'%2C%20alpha%3D0.6%2C%20label%3D'Whistle%20Density%20(Trend)')%0A%20%20%20%20ax2.set_ylabel('Whistle%20Density%20(Rolling)'%2C%20color%3Dcolor_density%2C%20alpha%3D0.8)%0A%20%20%20%20ax2.tick_params(axis%3D'y'%2C%20labelcolor%3Dcolor_density)%0A%0A%20%20%20%20%23%20Annotations%3A%20Period%20Transitions%0A%20%20%20%20for%20p%20in%20range(1%2C%205)%3A%0A%20%20%20%20%20%20%20%20period_mark%20%3D%20p%20*%20720%0A%20%20%20%20%20%20%20%20ax1.axvline(x%3Dperiod_mark%2C%20color%3D'black'%2C%20alpha%3D0.2%2C%20linestyle%3D'%3A')%0A%20%20%20%20%20%20%20%20if%20p%20%3C%205%3A%20ax1.text(period_mark%20-%20350%2C%20ax1.get_ylim()%5B1%5D%20*%200.9%2C%20f'Q%7Bp%7D'%2C%20alpha%3D0.4)%0A%0A%20%20%20%20%23%20Formatting%0A%20%20%20%20ax1.set_title('Temporal%20Officiating%3A%20Whistle%20Clusters%20vs.%20Game%20Momentum'%2C%20fontsize%3D18%2C%20pad%3D20)%0A%20%20%20%20ax1.set_xlabel('Seconds%20Elapsed%20(Game%20Total)'%2C%20fontsize%3D12)%0A%20%20%20%20ax1.set_ylabel('Score%20Margin%20(LAL%20%2B%20%2F%20GSW%20-)'%2C%20fontsize%3D12)%0A%20%20%20%20ax1.grid(True%2C%20alpha%3D0.1)%0A%0A%20%20%20%20%23%20Combined%20Legend%0A%20%20%20%20lines1%2C%20labels1%20%3D%20ax1.get_legend_handles_labels()%0A%20%20%20%20lines2%2C%20labels2%20%3D%20ax2.get_legend_handles_labels()%0A%20%20%20%20ax1.legend(lines1%20%2B%20lines2%2C%20labels1%20%2B%20labels2%2C%20bbox_to_anchor%3D(1.05%2C%201)%2C%20loc%3D'upper%20left'%2C%20title%3D'Legend')%0A%0A%20%20%20%20plt.tight_layout()%0A%20%20%20%20plt.show()%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
eeb233fe8242852489ecb52856e0cf88