wsba-hockey 1.1.0__py3-none-any.whl → 1.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. wsba_hockey/api/api/index.py +129 -0
  2. wsba_hockey/data_pipelines.py +71 -8
  3. wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/game_stats/app.py +6 -5
  4. wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/goalie/app.py +101 -0
  5. wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/goalie/plot.py +71 -0
  6. wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/goalie/rink_plot.py +245 -0
  7. wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/heatmaps/app.py +1 -1
  8. wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/heatmaps/plot.py +2 -0
  9. wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/heatmaps/rink_plot.py +1 -1
  10. wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/matchups/app.py +3 -3
  11. wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/matchups/plot.py +2 -0
  12. wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/matchups/rink_plot.py +1 -1
  13. wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/pbp/app.py +44 -28
  14. wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/pbp/plot.py +12 -3
  15. wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/pbp/rink_plot.py +1 -1
  16. wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/skater/app.py +1 -1
  17. wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/skater/plot.py +5 -4
  18. wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/skater/rink_plot.py +1 -1
  19. wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/team_heatmaps/app.py +103 -0
  20. wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/team_heatmaps/plot.py +95 -0
  21. wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/team_heatmaps/rink_plot.py +245 -0
  22. wsba_hockey/flask/app.py +77 -0
  23. wsba_hockey/tools/plotting.py +1 -0
  24. wsba_hockey/tools/scraping.py +6 -2
  25. wsba_hockey/tools/xg_model.py +1 -1
  26. wsba_hockey/workspace.py +28 -12
  27. wsba_hockey/wsba_main.py +10 -17
  28. {wsba_hockey-1.1.0.dist-info → wsba_hockey-1.1.2.dist-info}/METADATA +1 -1
  29. {wsba_hockey-1.1.0.dist-info → wsba_hockey-1.1.2.dist-info}/RECORD +32 -24
  30. {wsba_hockey-1.1.0.dist-info → wsba_hockey-1.1.2.dist-info}/WHEEL +0 -0
  31. {wsba_hockey-1.1.0.dist-info → wsba_hockey-1.1.2.dist-info}/licenses/LICENSE +0 -0
  32. {wsba_hockey-1.1.0.dist-info → wsba_hockey-1.1.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,103 @@
1
+ import pandas as pd
2
+ import plot as wsba_plt
3
+ import numpy as np
4
+ from urllib.parse import *
5
+ from shiny import *
6
+ from shinywidgets import output_widget, render_widget
7
+
8
+ app_ui = ui.page_fluid(
9
+ ui.tags.style(
10
+ "body {background:#09090b"
11
+ "}"
12
+ ),
13
+ output_widget("plot_skater"),
14
+ )
15
+
16
+ def server(input, output, session):
17
+ @output()
18
+ @render_widget
19
+ def plot_skater():
20
+ #Retreive query parameters
21
+ search = session.input[".clientdata_url_search"]()
22
+ query = parse_qs(urlparse(search).query)
23
+
24
+ print(query)
25
+ #If no input data is provided automatically provide a select skater and plot all 5v5 fenwick shots
26
+ defaults = {
27
+ 'team':['BOS'],
28
+ 'season':['20222023'],
29
+ 'strength_state':['5v5'],
30
+ 'season_type':['2']
31
+ }
32
+
33
+ for key in defaults.keys():
34
+ if key not in query.keys():
35
+ query.update({key:defaults[key]})
36
+
37
+ #Iterate through query and parse params with multiple selections
38
+ for param in query.keys():
39
+ q_string = query[param][0]
40
+ query[param] = q_string.split(',')
41
+
42
+ print(query)
43
+ #Determine which season to load based on the input
44
+ season = query['season'][0]
45
+ #Load appropriate dataframe
46
+ df = pd.read_parquet(f'https://weakside-breakout.s3.us-east-2.amazonaws.com/pbp/{season}.parquet')
47
+
48
+ #Prepare dataframe for plotting based on URL parameters
49
+ df = df.loc[(df['season'].astype(str).isin(query['season']))&(df['season_type'].astype(str).isin(query['season_type']))].replace({np.nan: None})
50
+ #Return empty rink if no data exists else continue
51
+ if df.empty:
52
+ return wsba_plt.wsba_rink()
53
+ else:
54
+ rink = wsba_plt.wsba_rink()
55
+
56
+ try:
57
+ for_plot = wsba_plt.heatmap(df,team=query['team'][0],events=['missed-shot','shot-on-goal','goal'],strengths=query['strength_state'],onice='for')
58
+ against_plot = wsba_plt.heatmap(df,team=query['team'][0],events=['missed-shot','shot-on-goal','goal'],strengths=query['strength_state'],onice='against')
59
+
60
+ for trace in for_plot.data:
61
+ rink.add_trace(trace)
62
+
63
+ for trace in against_plot.data:
64
+ rink.add_trace(trace)
65
+
66
+ season = int(season[0:4])
67
+ team = query['team'][0]
68
+ strengths = 'All Situations' if len(query['strength_state']) == 4 else query['strength_state']
69
+ span = 'Regular Season' if query['season_type'][0]=='2' else 'Playoffs'
70
+
71
+ return rink.update_layout(
72
+ title=dict(
73
+ text=f'{team} On-Ice xG at {strengths}; {season}-{season+1}, {span}',
74
+ x=0.5, y=0.96,
75
+ xanchor='center',
76
+ yanchor='top',
77
+ font=dict(color='white')
78
+ ),
79
+ ).add_annotation(
80
+ text='Lower xG',
81
+ xref="paper",
82
+ yref="paper",
83
+ xanchor='right',
84
+ yanchor='top',
85
+ font=dict(color='white'),
86
+ x=0.3,
87
+ y=0.04,
88
+ showarrow=False
89
+ ).add_annotation(
90
+ text='Higher xG',
91
+ xref="paper",
92
+ yref="paper",
93
+ xanchor='right',
94
+ yanchor='top',
95
+ font=dict(color='white'),
96
+ x=0.76,
97
+ y=0.04,
98
+ showarrow=False
99
+ )
100
+ except:
101
+ return wsba_plt.wsba_rink()
102
+
103
+ app = App(app_ui, server)
@@ -0,0 +1,95 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+ import plotly.graph_objects as go
4
+ import matplotlib.pyplot as plt
5
+ import rink_plot
6
+ from scipy.interpolate import griddata
7
+ from scipy.ndimage import gaussian_filter
8
+
9
+ def wsba_rink(setting='full', vertical=False):
10
+ return rink_plot.rink(setting=setting, vertical=vertical)
11
+
12
+ def heatmap(df,team,events,strengths,onice):
13
+ df['event_team_abbr_2'] = np.where(df['home_team_abbr']==df['event_team_abbr'],df['away_team_abbr'],df['home_team_abbr'])
14
+ df['strength_state_2'] = df['strength_state'].str[::-1]
15
+
16
+ df = df.fillna(0)
17
+ df = df.loc[(df['event_type'].isin(events))&(df['x_adj'].notna())&(df['y_adj'].notna())]
18
+ if onice == 'for':
19
+ df['x'] = abs(df['x_adj'])
20
+ df['y'] = np.where(df['x_adj']<0,-df['y_adj'],df['y_adj'])
21
+ df['event_distance'] = abs(df['event_distance'].fillna(0))
22
+ df = df.loc[(df['event_distance']<=89)&(df['x']<=89)&(df['empty_net']==0)]
23
+
24
+ x_min = 0
25
+ x_max = 100
26
+ else:
27
+ df['x'] = -abs(df['x_adj'])
28
+ df['y'] = np.where(df['x_adj']>0,-df['y_adj'],df['y_adj'])
29
+ df['event_distance'] = -abs(df['event_distance'])
30
+ df = df.loc[(df['event_distance']>-89)&(df['x']>-89)&(df['empty_net']==0)]
31
+
32
+ x_min = -100
33
+ x_max = 0
34
+
35
+ df['home_on_ice'] = df['home_on_1_id'].astype(str) + ";" + df['home_on_2_id'].astype(str) + ";" + df['home_on_3_id'].astype(str) + ";" + df['home_on_4_id'].astype(str) + ";" + df['home_on_5_id'].astype(str) + ";" + df['home_on_6_id'].astype(str)
36
+ df['away_on_ice'] = df['away_on_1_id'].astype(str) + ";" + df['away_on_2_id'].astype(str) + ";" + df['away_on_3_id'].astype(str) + ";" + df['away_on_4_id'].astype(str) + ";" + df['away_on_5_id'].astype(str) + ";" + df['away_on_6_id'].astype(str)
37
+
38
+ df['onice_for'] = np.where(df['home_team_abbr']==df['event_team_abbr'],df['home_on_ice'],df['away_on_ice'])
39
+ df['onice_against'] = np.where(df['away_team_abbr']==df['event_team_abbr'],df['home_on_ice'],df['away_on_ice'])
40
+
41
+ df['strength_state'] = np.where(df['strength_state'].isin(['5v5','5v4','4v5']),df['strength_state'],'Other')
42
+ df['strength_state_2'] = np.where(df['strength_state_2'].isin(['5v5','5v4','4v5']),df['strength_state_2'],'Other')
43
+
44
+ if strengths != 'all':
45
+ if onice == 'against':
46
+ df = df.loc[((df['strength_state_2'].isin(strengths)))]
47
+ else:
48
+ df = df.loc[((df['strength_state'].isin(strengths)))]
49
+
50
+ [x,y] = np.round(np.meshgrid(np.linspace(x_min,x_max,(x_max-x_min)),np.linspace(-42.5,42.5,85)))
51
+ xgoals = griddata((df['x'],df['y']),df['xG'],(x,y),method='cubic',fill_value=0)
52
+ xgoals = np.where(xgoals < 0,0,xgoals)
53
+ xgoals_smooth = gaussian_filter(xgoals,sigma=3)
54
+
55
+ if onice == 'for':
56
+ player_shots = df.loc[(df['event_team_abbr']==team)]
57
+ else:
58
+ player_shots = df.loc[(df['event_team_abbr_2']==team)]
59
+ [x,y] = np.round(np.meshgrid(np.linspace(x_min,x_max,(x_max-x_min)),np.linspace(-42.5,42.5,85)))
60
+ xgoals_player = griddata((player_shots['x'],player_shots['y']),player_shots['xG'],(x,y),method='cubic',fill_value=0)
61
+ xgoals_player = np.where(xgoals_player < 0,0,xgoals_player)
62
+
63
+ difference = (gaussian_filter(xgoals_player,sigma = 3)) - xgoals_smooth
64
+ data_min= difference.min()
65
+ data_max= difference.max()
66
+
67
+ if abs(data_min) > data_max:
68
+ data_max = data_min * -1
69
+ elif data_max > abs(data_min):
70
+ data_min = data_max * -1
71
+
72
+ fig = go.Figure(
73
+ data = go.Contour( x=np.linspace(x_min,x_max,(x_max-x_min)),
74
+ y=np.linspace(-42.5,42.5,85),
75
+ z=difference,
76
+ colorscale=[[0.0,'red'],[0.5,'#09090b'],[1.0,'blue']],
77
+ connectgaps=True,
78
+ contours=dict(
79
+ type='levels',
80
+ start = data_min,
81
+ end = data_max,
82
+ size=(data_max-data_min)/11
83
+ ),
84
+ colorbar=dict(
85
+ len = 0.7,
86
+ orientation='h',
87
+ showticklabels=False,
88
+ thickness=15,
89
+ yref='paper',
90
+ yanchor='top',
91
+ y=0
92
+ ))
93
+ )
94
+
95
+ return fig
@@ -0,0 +1,245 @@
1
+
2
+ import numpy as np
3
+ import plotly.graph_objects as go
4
+ import io
5
+ import base64
6
+ import requests as rs
7
+ from PIL import Image
8
+
9
+ def rink(setting = "full", vertical = False):
10
+ '''
11
+ Function to plot rink in Plotly. Takes 2 arguments :
12
+
13
+ setting : full (default) for full ice, offense positive half of the ice, ozone positive quarter of ice, defense for negative half of the ice, dzone for negative quarter of the ice, and neutral for the neutral zone
14
+ vertical : True if you want a vertical rink, False (default) is for an horizontal rink
15
+
16
+ '''
17
+
18
+ def faceoff_circle(x, y, outer=True):
19
+ segments = []
20
+ theta = np.linspace(0, 2*np.pi, 300)
21
+ if outer:
22
+ # Outer circle
23
+ x_outer = x + 15*np.cos(theta)
24
+ y_outer = y + 15*np.sin(theta)
25
+ outer_circle = go.Scatter(x=x_outer, y=y_outer, mode='lines', line=dict(width=2, color='red'), showlegend=False, hoverinfo='skip')
26
+
27
+ segments.append(outer_circle)
28
+
29
+ # Inner circle
30
+ x_inner = x + np.cos(theta)
31
+ y_inner = y + np.sin(theta)
32
+ inner_circle = go.Scatter(x=x_inner, y=y_inner, mode='lines', fill='toself', fillcolor='rgba(255, 0, 0, 0.43)', line=dict(color='rgba(255, 0, 0, 1)', width=2), showlegend=False, hoverinfo='skip')
33
+
34
+ segments.append(inner_circle)
35
+
36
+ return segments #segments
37
+
38
+ fig = go.Figure()
39
+
40
+ if vertical :
41
+ setting_dict = {
42
+ "full" : [-101, 101],
43
+ "offense" : [0, 101],
44
+ "ozone" : [25, 101],
45
+ "defense" : [-101, 0],
46
+ "dzone" : [-101, -25],
47
+ "neutral" : [-25,25]
48
+ }
49
+ fig.update_layout(xaxis=dict(range=[-42.6, 42.6], showgrid=False, zeroline=False, showticklabels=False, constrain="domain"), yaxis=dict(range=setting_dict[setting], showgrid=False, zeroline=False, showticklabels=False, constrain="domain"),
50
+ showlegend=False, autosize=True, template="plotly_white")
51
+ fig.update_yaxes(
52
+ scaleanchor="x",
53
+ scaleratio=1,
54
+ )
55
+ def goal_crease(flip=1):
56
+ x_seq = np.linspace(-4, 4, 100)
57
+ x_goal = np.concatenate(([-4], x_seq, [4]))
58
+ y_goal = flip * np.concatenate(([89], 83 + x_seq**2/4**2*1.5, [89]))
59
+ goal_crease = go.Scatter(x=x_goal, y=y_goal, fill='toself', fillcolor='rgba(173, 216, 230, 0.3)', line=dict(color='red'))
60
+ return goal_crease
61
+
62
+ # Outer circle
63
+ theta = np.linspace(0, 2*np.pi, 300)
64
+ x_outer = 15 * np.cos(theta)
65
+ y_outer = 15 * np.sin(theta)
66
+ fig.add_trace(go.Scatter(x=x_outer, y=y_outer, mode='lines', line=dict(color='royalblue', width=2), showlegend=False, hoverinfo='skip'))
67
+ # Inner circle
68
+ theta2 = np.linspace(np.pi/2, 3*np.pi/2, 300)
69
+ x_inner = 42.5 + 10 * np.cos(theta2)
70
+ y_inner = 10 * np.sin(theta2)
71
+ fig.add_trace(go.Scatter(x=x_inner, y=y_inner, mode='lines', line=dict(color='red', width=2), showlegend=False, hoverinfo='skip'))
72
+ # Rink boundaries
73
+ fig.add_shape(type='rect', xref='x', yref='y', x0=-42.5, y0=25, x1=42.5, y1=26, line=dict(color='royalblue', width=1), fillcolor='royalblue', opacity=1)
74
+ fig.add_shape(type='rect', xref='x', yref='y', x0=-42.5, y0=-25, x1=42.5, y1=-26, line=dict(color='royalblue', width=1), fillcolor='royalblue', opacity=1)
75
+ fig.add_shape(type='rect', xref='x', yref='y', x0=-42.5, y0=-0.5, x1=42.5, y1=0.5, line=dict(color='red', width=2), fillcolor='red')
76
+
77
+ # Goal crease
78
+ fig.add_trace(goal_crease())
79
+ fig.add_trace(goal_crease(-1))
80
+ # Goal lines
81
+ goal_line_extreme = 42.5 - 28 + np.sqrt(28**2 - (28-11)**2)
82
+ fig.add_shape(type='line', xref='x', yref='y', x0=-goal_line_extreme, y0=89, x1=goal_line_extreme, y1=89, line=dict(color='red', width=2))
83
+ fig.add_shape(type='line', xref='x', yref='y', x0=-goal_line_extreme, y0=-89, x1=goal_line_extreme, y1=-89, line=dict(color='red', width=2))
84
+
85
+ # Faceoff circles
86
+ fig.add_traces(faceoff_circle(-22, 69))
87
+ fig.add_traces(faceoff_circle(22, 69))
88
+ fig.add_traces(faceoff_circle(-22, -69))
89
+ fig.add_traces(faceoff_circle(22, -69))
90
+ fig.add_traces(faceoff_circle(-22, -20, False))
91
+ fig.add_traces(faceoff_circle(22, -20, False))
92
+ fig.add_traces(faceoff_circle(-22, 20, False))
93
+ fig.add_traces(faceoff_circle(22, 20, False))
94
+
95
+ # Sidelines
96
+ theta_lines = np.linspace(0, np.pi/2, 20)
97
+ x_lines1 = np.concatenate(([-42.5], -42.5 + 28 - 28*np.cos(theta_lines), 42.5 - 28 + 28*np.cos(np.flip(theta_lines))))
98
+ y_lines1 = np.concatenate(([15], 72 + 28*np.sin(theta_lines), 72 + 28*np.sin(np.flip(theta_lines))))
99
+ x_lines2 = np.concatenate(([-42.5], -42.5 + 28 - 28*np.cos(theta_lines), 42.5 - 28 + 28*np.cos(np.flip(theta_lines))))
100
+ y_lines2 = np.concatenate(([15], -72 - 28*np.sin(theta_lines), -72 - 28*np.sin(np.flip(theta_lines))))
101
+ fig.add_trace(go.Scatter(x=x_lines1, y=y_lines1, mode='lines', line=dict(color='white', width=2), showlegend=False, hoverinfo='skip'))
102
+ fig.add_trace(go.Scatter(x=x_lines2, y=y_lines2, mode='lines', line=dict(color='white', width=2), showlegend=False, hoverinfo='skip'))
103
+ fig.add_shape(type='line', xref='x', yref='y', x0=42.5, y0=-72.5, x1=42.5, y1=72.5, line=dict(color='white', width=2))
104
+ fig.add_shape(type='line', xref='x', yref='y', x0=-42.5, y0=-72.5, x1=-42.5, y1=72.5, line=dict(color='white', width=2))
105
+
106
+ # Add goals
107
+ goal_width = 6 # feet
108
+ goal_depth = 4 # feet
109
+
110
+ # Top goal
111
+ fig.add_shape(
112
+ type="rect",
113
+ xref="x",
114
+ yref="y",
115
+ x0=-goal_width / 2,
116
+ y0=89,
117
+ x1=goal_width / 2,
118
+ y1=89 + goal_depth,
119
+ line=dict(color="red", width=2),
120
+ )
121
+ # Bottom goal
122
+ fig.add_shape(
123
+ type="rect",
124
+ xref="x",
125
+ yref="y",
126
+ x0=-goal_width / 2,
127
+ y0=-89 - goal_depth,
128
+ x1=goal_width / 2,
129
+ y1=-89,
130
+ line=dict(color="red", width=2),
131
+ )
132
+
133
+ else :
134
+ setting_dict = {
135
+ "full" : [-101, 101],
136
+ "offense" : [0, 101],
137
+ "ozone" : [25, 101],
138
+ "defense" : [-101, 0],
139
+ "dzone" : [-101, -25]
140
+ }
141
+ fig.update_layout(xaxis=dict(range=setting_dict[setting], showgrid=False, zeroline=False, showticklabels=False), yaxis=dict(range=[-42.6, 42.6], showgrid=False, zeroline=False, showticklabels=False, constrain="domain"),
142
+ showlegend=True, autosize =True, template="plotly_white")
143
+ fig.update_yaxes(
144
+ scaleanchor="x",
145
+ scaleratio=1,
146
+ )
147
+ def goal_crease(flip=1):
148
+ y_seq = np.linspace(-4, 4, 100)
149
+ y_goal = np.concatenate(([-4], y_seq, [4]))
150
+ x_goal = flip * np.concatenate(([89], 83 + y_seq**2/4**2*1.5, [89]))
151
+ goal_crease = go.Scatter(x=x_goal, y=y_goal, fill='toself', fillcolor='rgba(173, 216, 230, 0.3)', line=dict(color='red'), showlegend=False, hoverinfo='skip')
152
+ return goal_crease
153
+
154
+ # Outer circle
155
+ theta = np.linspace(0, 2 * np.pi, 300)
156
+ x_outer = 15 * np.sin(theta)
157
+ y_outer = 15 * np.cos(theta)
158
+ fig.add_trace(go.Scatter(x=x_outer, y=y_outer, mode='lines', line=dict(color='royalblue', width=2), showlegend=False, hoverinfo='skip'))
159
+ # Inner circle
160
+ theta2 = np.linspace(3 * np.pi / 2, np.pi / 2, 300) # Update theta2 to rotate the plot by 180 degrees
161
+ x_inner = 10 * np.sin(theta2) # Update x_inner to rotate the plot by 180 degrees
162
+ y_inner = -42.5 - 10 * np.cos(theta2) # Update y_inner to rotate the plot by 180 degrees
163
+ fig.add_trace(go.Scatter(x=x_inner, y=y_inner, mode='lines', line=dict(color='red', width=2), showlegend=False, hoverinfo='skip'))
164
+
165
+ # Rink boundaries
166
+ fig.add_shape(type='rect', xref='x', yref='y', x0=25, y0=-42.5, x1=26, y1=42.5, line=dict(color='royalblue', width=1), fillcolor='royalblue', opacity=1)
167
+ fig.add_shape(type='rect', xref='x', yref='y', x0=-25, y0=-42.5, x1=-26, y1=42.5, line=dict(color='royalblue', width=1), fillcolor='royalblue', opacity=1)
168
+ fig.add_shape(type='rect', xref='x', yref='y', x0=-0.5, y0=-42.5, x1=0.5, y1=42.5, line=dict(color='red', width=2), fillcolor='red')
169
+ # Goal crease
170
+ fig.add_trace(goal_crease())
171
+ fig.add_trace(goal_crease(-1))
172
+ # Goal lines
173
+ goal_line_extreme = 42.5 - 28 + np.sqrt(28 ** 2 - (28 - 11) ** 2)
174
+ fig.add_shape(type='line', xref='x', yref='y', x0=89, y0=-goal_line_extreme, x1=89, y1=goal_line_extreme, line=dict(color='red', width=2))
175
+ fig.add_shape(type='line', xref='x', yref='y', x0=-89, y0=-goal_line_extreme, x1=-89, y1=goal_line_extreme, line=dict(color='red', width=2))
176
+ # Faceoff circles
177
+ fig.add_traces(faceoff_circle(-69, -22))
178
+ fig.add_traces(faceoff_circle(-69, 22))
179
+ fig.add_traces(faceoff_circle(69, -22))
180
+ fig.add_traces(faceoff_circle(69, 22))
181
+ fig.add_traces(faceoff_circle(-20, -22, False))
182
+ fig.add_traces(faceoff_circle(-20, 22, False))
183
+ fig.add_traces(faceoff_circle(20, -22, False))
184
+ fig.add_traces(faceoff_circle(20, 22, False))
185
+
186
+ # Sidelines
187
+ theta_lines = np.linspace(0, np.pi / 2, 20)
188
+ x_lines1 = np.concatenate(([15], 72 + 28 * np.sin(theta_lines), 72 + 28 * np.sin(np.flip(theta_lines))))
189
+ y_lines1 = np.concatenate(([-42.5], -42.5 + 28 - 28 * np.cos(theta_lines), 42.5 - 28 + 28 * np.cos(np.flip(theta_lines))))
190
+ x_lines2 = np.concatenate(([15], -72 - 28 * np.sin(theta_lines), -72 - 28 * np.sin(np.flip(theta_lines))))
191
+ y_lines2 = np.concatenate(([-42.5], -42.5 + 28 - 28 * np.cos(theta_lines), 42.5 - 28 + 28 * np.cos(np.flip(theta_lines))))
192
+ fig.add_trace(go.Scatter(x=x_lines1, y=y_lines1, mode='lines', line=dict(color='white', width=2), showlegend=False, hoverinfo='skip'))
193
+ fig.add_trace(go.Scatter(x=x_lines2, y=y_lines2, mode='lines', line=dict(color='white', width=2), showlegend=False, hoverinfo='skip'))
194
+ fig.add_shape(type='line', xref='x', yref='y', x0=-72.5, y0=-42.5, x1=72.5, y1=-42.5, line=dict(color='white', width=2))
195
+ fig.add_shape(type='line', xref='x', yref='y', x0=-72.5, y0=42.5, x1=72.5, y1=42.5, line=dict(color='white', width=2))
196
+
197
+ # Add goals
198
+ goal_width = 6 # feet
199
+ goal_depth = 4 # feet
200
+
201
+ # Right goal
202
+ fig.add_shape(
203
+ type="rect",
204
+ xref="x",
205
+ yref="y",
206
+ x0=89,
207
+ y0=-goal_width / 2,
208
+ x1=89 + goal_depth,
209
+ y1=goal_width / 2,
210
+ line=dict(color="red", width=2),
211
+ )
212
+ # Left goal
213
+ fig.add_shape(
214
+ type="rect",
215
+ xref="x",
216
+ yref="y",
217
+ x0=-89 - goal_depth,
218
+ y0=-goal_width / 2,
219
+ x1=-89,
220
+ y1=goal_width / 2,
221
+ line=dict(color="red", width=2),
222
+ )
223
+
224
+ # Add logo
225
+ logo = Image.open(rs.get('https://weakside-breakout.s3.us-east-2.amazonaws.com/utils/wsba.png',stream=True).raw)
226
+
227
+ fig.add_layout_image(
228
+ dict(
229
+ source=logo,
230
+ xref="x",
231
+ yref="y",
232
+ x=-12,
233
+ y=12,
234
+ sizex=24,
235
+ sizey=24,
236
+ sizing="stretch",
237
+ opacity=1)
238
+ )
239
+
240
+ #Set background to transparent
241
+ fig.update_layout(
242
+ paper_bgcolor="rgba(0,0,0,0)",
243
+ plot_bgcolor="rgba(0,0,0,0)"
244
+ )
245
+ return fig
@@ -0,0 +1,77 @@
1
+ from flask import Flask, render_template, request, redirect
2
+ from flask_sqlalchemy import SQLAlchemy
3
+ import pandas as pd
4
+
5
+ app = Flask(__name__)
6
+
7
+ #Globals
8
+ seasons = [
9
+ '20102011',
10
+ '20112012',
11
+ '20122013',
12
+ '20132014',
13
+ '20142015',
14
+ '20152016',
15
+ '20162017',
16
+ '20172018',
17
+ '20182019',
18
+ '20192020',
19
+ '20202021',
20
+ '20212022',
21
+ '20222023',
22
+ '20232024',
23
+ '20242025'
24
+ ]
25
+
26
+ #Generate pages
27
+ @app.route("/")
28
+ def index():
29
+ return render_template("index.html")
30
+
31
+ @app.route("/about/about")
32
+ def about():
33
+ return render_template("about/about.html")
34
+
35
+ @app.route("/about/glossary")
36
+ def glossary():
37
+ return render_template("about/glossary.html")
38
+
39
+ @app.route("/about/goal_impact")
40
+ def goal_impact():
41
+ return render_template("about/goal_impact.html")
42
+
43
+ @app.route("/about/resources")
44
+ def resources():
45
+ return render_template("about/resources.html")
46
+
47
+ @app.route("/about/xg_model")
48
+ def xg_model():
49
+ return render_template("about/xg_model.html")
50
+
51
+ @app.route("/games/schedule")
52
+ def schedule():
53
+ return render_template("games/schedule.html")
54
+
55
+ @app.route("/games/game_metrics")
56
+ def pbp_viewer():
57
+ return render_template("games/game_metrics.html")
58
+
59
+ @app.route("/players/skater_stats", methods=["GET", "POST"])
60
+ def skater_stats():
61
+ filters = {}
62
+ for filter in ['season','span','strength','position','display','type','min_age','min_toi']:
63
+ print(request.args.get(filter))
64
+ filters.update({filter:request.args.get(filter)})
65
+
66
+ return render_template("players/skater_stats.html")
67
+
68
+ @app.route("/players/goalie_stats")
69
+ def goalie_stats():
70
+ return render_template("players/goalie_stats.html")
71
+
72
+ @app.route("/players/team_stats")
73
+ def team_stats():
74
+ return render_template("players/team_stats.html")
75
+
76
+ if __name__ == "__main__":
77
+ app.run()
@@ -1,3 +1,4 @@
1
+ import os
1
2
  import matplotlib.pyplot as plt
2
3
  import numpy as np
3
4
  import pandas as pd
@@ -28,7 +28,7 @@ def get_col():
28
28
  return [
29
29
  'season','season_type','game_id','game_date',"start_time","venue","venue_location",
30
30
  'away_team_abbr','home_team_abbr','event_num','period','period_type',
31
- 'seconds_elapsed',"strength_state","strength_state_venue","home_team_defending_side",
31
+ 'seconds_elapsed','period_time','game_time',"strength_state","strength_state_venue","home_team_defending_side",
32
32
  "event_type_code","event_type","description","event_reason",
33
33
  "penalty_type","penalty_duration","penalty_attribution",
34
34
  "event_team_abbr","event_team_venue",
@@ -984,7 +984,11 @@ def combine_data(info,sources):
984
984
  df[f'{venue}_corsi'] = ((df['event_team_venue']==venue)&(df['event_type'].isin(['blocked-shot','missed-shot','shot-on-goal','goal']))).cumsum()
985
985
  df[f'{venue}_fenwick'] = ((df['event_team_venue']==venue)&(df['event_type'].isin(['missed-shot','shot-on-goal','goal']))).cumsum()
986
986
  df[f'{venue}_penalties'] = ((df['event_team_venue']==venue)&(df['event_type']=='penalty')).cumsum()
987
-
987
+
988
+ #Add time adjustments
989
+ df['period_time'] = np.trunc((df['seconds_elapsed']-((df['period']-1)*1200))/60).astype(str).str.replace('.0','')+":"+(df['seconds_elapsed'] % 60).astype(str).str.pad(2,'left','0')
990
+ df['game_time'] = np.trunc(df['seconds_elapsed']/60).astype(str).str.replace('.0','')+":"+(df['seconds_elapsed'] % 60).astype(str).str.pad(2,'left','0')
991
+
988
992
  #Forward fill as necessary
989
993
  cols = ['period_type','home_team_defending_side','away_coach','home_coach']
990
994
  for col in cols:
@@ -118,7 +118,7 @@ def fix_players(pbp):
118
118
 
119
119
  def prep_xG_data(data):
120
120
  #Prep data for xG training and calculation
121
- data = fix_players(data)
121
+ #data = fix_players(data)
122
122
 
123
123
  #Informal groupby
124
124
  data = data.sort_values(by=['season','game_id','period','seconds_elapsed','event_num'])
wsba_hockey/workspace.py CHANGED
@@ -6,30 +6,46 @@ import numpy as np
6
6
 
7
7
  season_load = wsba.repo_load_seasons()
8
8
 
9
- select = season_load[3:18]
10
-
11
- #data.pbp(select)
12
- #data.pbp_db(select)
9
+ select = season_load[9:17]
13
10
 
14
11
  #pbp = data.load_pbp_db(select)
15
12
 
16
13
  #wsba.wsba_xG(pbp,hypertune=True,train=True,train_runs=30,cv_runs=30)
14
+ #select = season_load[3:18]
17
15
  #for season in select:
18
16
  # wsba.nhl_apply_xG(data.load_pbp([season])).to_parquet(f'pbp/parquet/nhl_pbp_{season}.parquet',index=False)
19
17
  #data.pbp_db(select)
20
- test = pd.read_parquet('backblaze_pbp/20242025.parquet')
21
- test.loc[(test['event_goalie_id']==8476945)].to_csv('Hellebuyck.csv',index=False)
22
18
 
23
- mp = pd.read_csv('shots_2024.csv')
24
- mp.loc[(mp['goalieIdForShot']==8476945)].to_csv('mfreally.csv',index=False)
19
+ #test = pd.read_parquet('aws_pbp/20242025.parquet')
25
20
  #wsba.roc_auc_curve(test,'tools/xg_model/wsba_xg.joblib')
26
21
  #wsba.feature_importance('tools/xg_model/wsba_xg.joblib')
27
22
  #wsba.reliability(test,'tools/xg_model/wsba_xg.joblib')
28
23
 
29
- ## UPLOAD TO BACKBLAZE ##
30
-
31
- #data.stats(['goalie'],select)
32
- #data.game_log(['goalie'],select)
24
+ #data.build_stats(['skater','team','goalie'],select)
25
+ #data.game_log(['skater','goalie'],select)
26
+ #data.fix_names(['skater','goalie'],select)
33
27
 
28
+ ## DATA EXPORT ##
34
29
  #data.push_to_sheet(select,['skaters','team','info'])
35
30
 
31
+ wsba.nhl_scrape_game(['2024020008'],remove=[]).to_csv('wtfwhy.csv',index=False)
32
+
33
+ pbp = pd.read_parquet('pbp/parquet/nhl_pbp_20242025.parquet')
34
+ helle = pbp.loc[pbp['event_goalie_id']==8476945,
35
+ ['game_id','period','seconds_elapsed',
36
+ 'strength_state','event_type','description',
37
+ 'event_goalie_id','x','y','xG']]
38
+ mp = pd.read_csv('shots_2024.csv')
39
+ goalie = mp.loc[mp['goalieIdForShot']==8476945,
40
+ ['game_id','period','time','event','goalieIdForShot',
41
+ 'xCord','yCord','xGoal']].replace({
42
+ 'SHOT':'shot-on-goal',
43
+ 'MISS':'missed-shot',
44
+ 'GOAL':'goal'
45
+ })
46
+
47
+ helle.to_csv('hellebuyck.csv',index=False)
48
+ helle['game_id'] = helle['game_id'].astype(str)
49
+ goalie['game_id'] = ('20240'+goalie['game_id'].astype(str))
50
+ pd.merge(helle,goalie,how='left',left_on=['game_id','period','seconds_elapsed','event_type','x','y'],right_on=['game_id','period','time','event','xCord','yCord']).to_csv('test.csv',index=False)
51
+