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'0022500630'%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%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%3D'%2300538C'%2C%20alpha%3D0.15%2C%20label%3D'DAL%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%3D'%23FFC72C'%2C%20alpha%3D0.15%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%0A%20%20%20%20%20%20%20%20palette%3D%7B'DAL'%3A%20'%2300538C'%2C%20'GSW'%3A%20'%23FFC72C'%7D%2C%0A%20%20%20%20%20%20%20%20edgecolor%3D'black'%2C%0A%20%20%20%20%20%20%20%20linewidth%3D1.5%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%3D'red'%2C%20linestyle%3D'--'%2C%20alpha%3D0.4%2C%20label%3D'Whistle%20Density%20(Trend)')%0A%20%20%20%20ax2.set_ylabel('Whistle%20Density%20(Rolling)'%2C%20color%3D'red'%2C%20alpha%3D0.6)%0A%20%20%20%20ax2.tick_params(axis%3D'y'%2C%20labelcolor%3D'red')%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.3%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.5)%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(DAL%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%0A%40app.cell%0Adef%20_(get_ref%2C%20opp_map%2C%20pbp_df%2C%20pd)%3A%0A%20%20%20%20import%20plotly.graph_objects%20as%20go%0A%20%20%20%20from%20plotly.subplots%20import%20make_subplots%0A%0A%20%20%20%20%23%201.%20Data%20Prep%20%26%20Scoring%20Logic%0A%20%20%20%20df_plot%20%3D%20pbp_df.copy()%0A%20%20%20%20df_plot%5B'scoreHome'%5D%20%3D%20pd.to_numeric(df_plot%5B'scoreHome'%5D%2C%20errors%3D'coerce').ffill().fillna(0)%0A%20%20%20%20df_plot%5B'scoreAway'%5D%20%3D%20pd.to_numeric(df_plot%5B'scoreAway'%5D%2C%20errors%3D'coerce').ffill().fillna(0)%0A%20%20%20%20df_plot%5B'score_margin'%5D%20%3D%20df_plot%5B'scoreHome'%5D%20-%20df_plot%5B'scoreAway'%5D%0A%0A%20%20%20%20def%20get_total_seconds_2(row)%3A%0A%20%20%20%20%20%20%20%20clock_str%20%3D%20str(row%5B'clock'%5D).replace('PT'%2C%20'').replace('M'%2C%20'%3A').replace('S'%2C%20'')%0A%20%20%20%20%20%20%20%20parts%20%3D%20clock_str.split('%3A')%0A%20%20%20%20%20%20%20%20try%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20len(parts)%20%3D%3D%202%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20rem%20%3D%20int(parts%5B0%5D)%20*%2060%20%2B%20float(parts%5B1%5D)%0A%20%20%20%20%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20rem%20%3D%20float(parts%5B0%5D)%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20((int(row%5B'period'%5D)%20-%201)%20*%20720)%20%2B%20(720%20-%20rem)%0A%20%20%20%20%20%20%20%20except%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%200%0A%0A%20%20%20%20df_plot%5B'seconds_elapsed'%5D%20%3D%20df_plot.apply(get_total_seconds_2%2C%20axis%3D1)%0A%0A%20%20%20%20%23%202.%20Process%20Whistle%20Events%20%26%20Assign%20Referee%20Shapes%0A%20%20%20%20df_foul_plot_2%20%3D%20df_plot%5Bdf_plot%5B'description'%5D.str.contains(r'%5C(%5Cw%5C.%5Cw%2B%5C)'%2C%20na%3DFalse)%5D.copy()%0A%20%20%20%20df_foul_plot_2%5B'Beneficiary'%5D%20%3D%20df_foul_plot_2%5B'teamTricode'%5D.map(opp_map)%0A%20%20%20%20df_foul_plot_2%5B'Referee'%5D%20%3D%20df_foul_plot_2%5B'description'%5D.apply(get_ref)%0A%0A%20%20%20%20%23%20Define%20a%20mapping%20for%20Referee%20Shapes%20(Circle%2C%20X%2C%20Square)%0A%20%20%20%20unique_refs%20%3D%20df_foul_plot_2%5B'Referee'%5D.unique()%0A%20%20%20%20shape_map%20%3D%20%7Bref%3A%20shape%20for%20ref%2C%20shape%20in%20zip(unique_refs%2C%20%5B'circle'%2C%20'x'%2C%20'square'%2C%20'diamond'%2C%20'cross'%5D)%7D%0A%20%20%20%20df_foul_plot_2%5B'ref_shape'%5D%20%3D%20df_foul_plot_2%5B'Referee'%5D.map(shape_map)%0A%0A%20%20%20%20%23%203.%20Calculate%20Rolling%20Density%0A%20%20%20%20df_plot%5B'is_whistle'%5D%20%3D%20df_plot.index.isin(df_foul_plot_2.index).astype(int)%0A%20%20%20%20df_plot%5B'whistle_density'%5D%20%3D%20df_plot%5B'is_whistle'%5D.rolling(window%3D60%2C%20min_periods%3D1).sum()%0A%0A%20%20%20%20%23%204.%20Create%20the%20Plot%0A%20%20%20%20fig_2%20%3D%20make_subplots(specs%3D%5B%5B%7B%22secondary_y%22%3A%20True%7D%5D%5D)%0A%0A%20%20%20%20%23%20Lead%20Areas%20(Matches%20the%20light%20blue%2Fyellow%20aesthetic)%0A%20%20%20%20fig_2.add_trace(go.Scatter(%0A%20%20%20%20%20%20%20%20x%3Ddf_plot%5B'seconds_elapsed'%5D%2C%20y%3Ddf_plot%5B'score_margin'%5D.clip(lower%3D0)%2C%0A%20%20%20%20%20%20%20%20fill%3D'tozeroy'%2C%20name%3D'DAL%20Lead'%2C%20line_color%3D'rgba(0%2C83%2C140%2C0.2)'%2C%0A%20%20%20%20%20%20%20%20fillcolor%3D'rgba(0%2C83%2C140%2C0.15)'%2C%20mode%3D'lines'%2C%20hoverinfo%3D'skip'%0A%20%20%20%20)%2C%20secondary_y%3DFalse)%0A%0A%20%20%20%20fig_2.add_trace(go.Scatter(%0A%20%20%20%20%20%20%20%20x%3Ddf_plot%5B'seconds_elapsed'%5D%2C%20y%3Ddf_plot%5B'score_margin'%5D.clip(upper%3D0)%2C%0A%20%20%20%20%20%20%20%20fill%3D'tozeroy'%2C%20name%3D'GSW%20Lead'%2C%20line_color%3D'rgba(255%2C199%2C44%2C0.2)'%2C%0A%20%20%20%20%20%20%20%20fillcolor%3D'rgba(255%2C199%2C44%2C0.1)'%2C%20mode%3D'lines'%2C%20hoverinfo%3D'skip'%0A%20%20%20%20)%2C%20secondary_y%3DFalse)%0A%0A%20%20%20%20%23%20Whistle%20Density%20Trend%20Line%20(Red%20dashed)%0A%20%20%20%20fig_2.add_trace(go.Scatter(%0A%20%20%20%20%20%20%20%20x%3Ddf_plot%5B'seconds_elapsed'%5D%2C%20y%3Ddf_plot%5B'whistle_density'%5D%2C%0A%20%20%20%20%20%20%20%20name%3D'Whistle%20Density%20(Trend)'%2C%20line%3Ddict(color%3D'%23ff6b6b'%2C%20width%3D1.5%2C%20dash%3D'dash')%2C%0A%20%20%20%20)%2C%20secondary_y%3DTrue)%0A%0A%20%20%20%20%23%20Individual%20Whistles%20(Grouped%20by%20Referee%20to%20get%20the%20Legend%20right)%0A%20%20%20%20for%20ref%20in%20unique_refs%3A%0A%20%20%20%20%20%20%20%20ref_df%20%3D%20df_foul_plot_2%5Bdf_foul_plot_2%5B'Referee'%5D%20%3D%3D%20ref%5D%0A%20%20%20%20%20%20%20%20for%20team%2C%20color%20in%20%5B('DAL'%2C%20'%2300538C')%2C%20('GSW'%2C%20'%23FFC72C')%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20team_ref_df%20%3D%20ref_df%5Bref_df%5B'Beneficiary'%5D%20%3D%3D%20team%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20fig_2.add_trace(go.Scatter(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20x%3Dteam_ref_df%5B'seconds_elapsed'%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20y%3D%5B0%5D%20*%20len(team_ref_df)%2C%20%23%20Markers%20sit%20on%20the%20zero-line%20like%20the%20reference%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20mode%3D'markers'%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%3Df'%7Bref%7D%20(%7Bteam%7D)'%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20marker%3Ddict(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20symbol%3Dshape_map%5Bref%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20size%3D12%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20color%3Dcolor%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20line%3Ddict(width%3D1.5%2C%20color%3D'black')%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20customdata%3Dteam_ref_df%5B%5B'description'%2C%20'clock'%2C%20'period'%5D%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20hovertemplate%3D%22%3Cb%3ERef%3A%20%22%20%2B%20ref%20%2B%20%22%3C%2Fb%3E%3Cbr%3EPlay%3A%20%25%7Bcustomdata%5B0%5D%7D%3Cextra%3E%3C%2Fextra%3E%22%0A%20%20%20%20%20%20%20%20%20%20%20%20)%2C%20secondary_y%3DFalse)%0A%0A%20%20%20%20%23%205.%20Styling%20%26%20Annotations%0A%20%20%20%20fig_2.update_layout(%0A%20%20%20%20%20%20%20%20title%3D'%3Cb%3ETemporal%20Officiating%3A%20Whistle%20Clusters%20vs.%20Game%20Momentum%3C%2Fb%3E'%2C%0A%20%20%20%20%20%20%20%20xaxis%3Ddict(title%3D'Seconds%20Elapsed%20(Game%20Total)'%2C%20showgrid%3DTrue%2C%20gridcolor%3D'%23f0f0f0')%2C%0A%20%20%20%20%20%20%20%20yaxis%3Ddict(title%3D'Score%20Margin%20(DAL%20%2B%20%2F%20GSW%20-)'%2C%20zeroline%3DTrue%2C%20zerolinecolor%3D'black'%2C%20zerolinewidth%3D1)%2C%0A%20%20%20%20%20%20%20%20yaxis2%3Ddict(title%3D'Whistle%20Density%20(Rolling)'%2C%20color%3D'%23ff6b6b'%2C%20range%3D%5B0%2C%2015%5D)%2C%0A%20%20%20%20%20%20%20%20template%3D'plotly_white'%2C%0A%20%20%20%20%20%20%20%20legend%3Ddict(title%3D%22Legend%22%2C%20orientation%3D%22v%22%2C%20x%3D1.1%2C%20y%3D1)%2C%0A%20%20%20%20%20%20%20%20width%3D1100%2C%20height%3D600%0A%20%20%20%20)%0A%0A%20%20%20%20%23%20Add%20Quarter%20Labels%20(Q1-Q4)%0A%20%20%20%20for%20i%2C%20label%20in%20enumerate(%5B'Q1'%2C%20'Q2'%2C%20'Q3'%2C%20'Q4'%5D)%3A%0A%20%20%20%20%20%20%20%20fig_2.add_annotation(x%3D(i*720)%2B360%2C%20y%3D11%2C%20text%3Dlabel%2C%20showarrow%3DFalse%2C%20font%3Ddict(color%3D'grey'))%0A%20%20%20%20%20%20%20%20if%20i%20%3E%200%3A%20fig_2.add_vline(x%3Di*720%2C%20line_width%3D1%2C%20line_dash%3D%22dot%22%2C%20line_color%3D%22grey%22)%0A%0A%20%20%20%20fig_2.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
20c371b410839a825f2d0386d733bbb9