wsba-hockey 1.0.6__py3-none-any.whl → 1.1.1__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.
- wsba_hockey/api/api/index.py +129 -0
- wsba_hockey/api/api/main.py +4 -0
- wsba_hockey/api/api/tools/__init__.py +0 -0
- wsba_hockey/api/api/tools/agg.py +374 -0
- wsba_hockey/api/api/tools/archive/old_scraping.py +1104 -0
- wsba_hockey/api/api/tools/plotting.py +144 -0
- wsba_hockey/api/api/tools/scraping.py +1000 -0
- wsba_hockey/api/api/tools/utils/__init__.py +1 -0
- wsba_hockey/api/api/tools/utils/config.py +14 -0
- wsba_hockey/api/api/tools/utils/save_pages.py +133 -0
- wsba_hockey/api/api/tools/utils/shared.py +450 -0
- wsba_hockey/api/api/tools/xg_model.py +455 -0
- wsba_hockey/api/api/wsba_main.py +1213 -0
- wsba_hockey/data_pipelines.py +71 -8
- wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/game_stats/app.py +6 -5
- wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/goalie/app.py +101 -0
- wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/goalie/plot.py +71 -0
- wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/goalie/rink_plot.py +245 -0
- wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/heatmaps/app.py +1 -1
- wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/heatmaps/plot.py +2 -0
- wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/heatmaps/rink_plot.py +1 -1
- wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/matchups/app.py +3 -3
- wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/matchups/plot.py +2 -0
- wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/matchups/rink_plot.py +1 -1
- wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/pbp/app.py +44 -28
- wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/pbp/plot.py +12 -3
- wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/pbp/rink_plot.py +1 -1
- wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/skater/app.py +1 -1
- wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/skater/plot.py +5 -4
- wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/skater/rink_plot.py +1 -1
- wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/team_heatmaps/app.py +103 -0
- wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/team_heatmaps/plot.py +95 -0
- wsba_hockey/evidence/weakside-breakout/wsba_nhl_apps/wsba_nhl_apps/team_heatmaps/rink_plot.py +245 -0
- wsba_hockey/flask/app.py +77 -0
- wsba_hockey/tools/plotting.py +3 -3
- wsba_hockey/tools/scraping.py +7 -3
- wsba_hockey/tools/xg_model.py +3 -3
- wsba_hockey/workspace.py +28 -12
- wsba_hockey/wsba_main.py +10 -17
- {wsba_hockey-1.0.6.dist-info → wsba_hockey-1.1.1.dist-info}/METADATA +1 -1
- {wsba_hockey-1.0.6.dist-info → wsba_hockey-1.1.1.dist-info}/RECORD +44 -24
- {wsba_hockey-1.0.6.dist-info → wsba_hockey-1.1.1.dist-info}/WHEEL +0 -0
- {wsba_hockey-1.0.6.dist-info → wsba_hockey-1.1.1.dist-info}/licenses/LICENSE +0 -0
- {wsba_hockey-1.0.6.dist-info → wsba_hockey-1.1.1.dist-info}/top_level.txt +0 -0
wsba_hockey/data_pipelines.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import os
|
1
2
|
import numpy as np
|
2
3
|
import pandas as pd
|
3
4
|
import wsba_main as wsba
|
@@ -23,15 +24,16 @@ def pbp_db(seasons):
|
|
23
24
|
for season in seasons:
|
24
25
|
pbp = pd.read_parquet(f'pbp/parquet/nhl_pbp_{season}.parquet')
|
25
26
|
pbp.loc[pbp['event_type'].isin(wsba.events+['penalty'])].to_csv('temp.csv',index=False)
|
26
|
-
pd.read_csv('temp.csv').to_parquet(f'
|
27
|
+
pd.read_csv('temp.csv').to_parquet(f'aws_pbp/{season}.parquet',index=False)
|
28
|
+
os.remove('temp.csv')
|
27
29
|
|
28
30
|
def load_pbp(seasons):
|
29
31
|
return pd.concat([pd.read_parquet(f'pbp/parquet/nhl_pbp_{season}.parquet') for season in seasons])
|
30
32
|
|
31
33
|
def load_pbp_db(seasons):
|
32
|
-
return pd.concat([pd.read_parquet(f'
|
34
|
+
return pd.concat([pd.read_parquet(f'aws_pbp/{season}.parquet') for season in seasons])
|
33
35
|
|
34
|
-
def
|
36
|
+
def build_stats(arg,seasons):
|
35
37
|
#Stats building
|
36
38
|
for group in arg:
|
37
39
|
for season in seasons:
|
@@ -73,9 +75,70 @@ def game_log(arg,seasons):
|
|
73
75
|
data['Span'] = season_type[0]
|
74
76
|
dfs.append(data)
|
75
77
|
stat = pd.concat(dfs)
|
76
|
-
|
77
|
-
|
78
|
-
stats.
|
78
|
+
path = 'stats/game_log' if group == 'skater' else 'stats/game_log/goalie'
|
79
|
+
stat.to_csv(f'{path}/temp.csv',index=False)
|
80
|
+
stats = pd.read_csv(f'{path}/temp.csv')
|
81
|
+
os.remove('temp.csv')
|
82
|
+
stats.to_parquet(f'{path}/wsba_nhl_{season}_game_log{'_goalie' if group == 'goalie' else ''}.parquet',index=False)
|
83
|
+
|
84
|
+
def fix_names(arg,seasons):
|
85
|
+
#Stats building
|
86
|
+
for group in arg:
|
87
|
+
for season in seasons:
|
88
|
+
print(f'Fixing names for {group} stats in {season}...')
|
89
|
+
|
90
|
+
group_name = 'Player' if 'skater' in group else 'Goalie'
|
91
|
+
if 'game_log' in group:
|
92
|
+
if 'skater' in group:
|
93
|
+
path = f'stats/{group[-8:]}/wsba_nhl_{season}_game_log.parquet'
|
94
|
+
else:
|
95
|
+
path = f'stats/{group[-8:]}/goalie/wsba_nhl_{season}_game_log_goalie.parquet'
|
96
|
+
else:
|
97
|
+
path = f'stats/{group}/wsba_nhl_{season}_{group}.csv'
|
98
|
+
|
99
|
+
if 'game_log' in group:
|
100
|
+
stats = pd.read_parquet(path)
|
101
|
+
else:
|
102
|
+
stats = pd.read_csv(path)
|
103
|
+
|
104
|
+
missing = stats.loc[stats[group_name].astype(str)=='0','ID'].drop_duplicates()
|
105
|
+
|
106
|
+
if not missing.to_list():
|
107
|
+
''
|
108
|
+
else:
|
109
|
+
info = wsba.nhl_scrape_player_data(missing)
|
110
|
+
columns={'playerId':'ID',
|
111
|
+
'fullName':group_name,
|
112
|
+
'position':'Position',
|
113
|
+
'headshot':'Headshot',
|
114
|
+
'shootsCatches':'Handedness',
|
115
|
+
'heightInInches':'Height (in)',
|
116
|
+
'weightInPounds':'Weight (lbs)',
|
117
|
+
'birthDate':'Birthday' }
|
118
|
+
|
119
|
+
info = info[list(columns.keys())]
|
120
|
+
complete = pd.merge(stats,info,how='left',left_on=['ID'],right_on=['playerId']).replace({'0':np.nan})
|
121
|
+
|
122
|
+
for key, value in zip(columns.keys(), columns.values()):
|
123
|
+
complete[value] = complete[value].combine_first(complete[key])
|
124
|
+
complete = complete.drop(columns=[key])
|
125
|
+
|
126
|
+
complete.to_csv('wtf.csv')
|
127
|
+
#Add player age
|
128
|
+
complete['Birthday'] = pd.to_datetime(complete['Birthday'],format='mixed')
|
129
|
+
complete['season_year'] = complete['Season'].astype(str).str[4:8].astype(int)
|
130
|
+
complete['Age'] = complete['season_year'] - complete['Birthday'].dt.year
|
131
|
+
|
132
|
+
complete['WSBA'] = complete[group_name]+complete['Team']+complete['Season'].astype(str)
|
133
|
+
complete = complete.sort_values(by=['Player','Season','Team','ID'])
|
134
|
+
|
135
|
+
if 'game_log' in group:
|
136
|
+
complete.to_csv('temp.csv',index=False)
|
137
|
+
pd.read_csv('temp.csv').to_parquet(path,index=False)
|
138
|
+
os.remove('temp.csv')
|
139
|
+
|
140
|
+
else:
|
141
|
+
complete.to_csv(path)
|
79
142
|
|
80
143
|
def push_to_sheet(seasons, types = ['skaters','team','goalie','info'], msg = 'Data Update'):
|
81
144
|
spread = Spread('WSBA - NHL 5v5 Shooting Metrics Public v1.0')
|
@@ -100,8 +163,8 @@ def push_to_sheet(seasons, types = ['skaters','team','goalie','info'], msg = 'Da
|
|
100
163
|
spread.df_to_sheet(goalie,index=False,sheet='Goalie DB')
|
101
164
|
|
102
165
|
if 'info' in types:
|
103
|
-
team_info = pd.read_csv('teaminfo/nhl_teaminfo.csv')
|
104
|
-
country = pd.read_csv('teaminfo/nhl_countryinfo.csv')
|
166
|
+
team_info = pd.read_csv('tools/teaminfo/nhl_teaminfo.csv')
|
167
|
+
country = pd.read_csv('tools/teaminfo/nhl_countryinfo.csv')
|
105
168
|
|
106
169
|
spread.df_to_sheet(team_info,index=False,sheet='Team Info')
|
107
170
|
spread.df_to_sheet(country,index=False,sheet='Country Info')
|
@@ -125,7 +125,7 @@ def server(input, output, session):
|
|
125
125
|
game_info = reactive.Value(None)
|
126
126
|
|
127
127
|
def get_schedule():
|
128
|
-
games = pd.read_csv('https://
|
128
|
+
games = pd.read_csv('https://weakside-breakout.s3.us-east-2.amazonaws.com/info/schedule.csv')
|
129
129
|
|
130
130
|
return games.loc[games['gameState'].isin(['OFF','FINAL'])]
|
131
131
|
|
@@ -139,6 +139,7 @@ def server(input, output, session):
|
|
139
139
|
|
140
140
|
defaults = {
|
141
141
|
'game_id':['2024020001'],
|
142
|
+
'title':['true']
|
142
143
|
}
|
143
144
|
|
144
145
|
for key in defaults.keys():
|
@@ -180,9 +181,9 @@ def server(input, output, session):
|
|
180
181
|
info = game_info.get()
|
181
182
|
season = info['season']
|
182
183
|
#Load appropriate dataframe
|
183
|
-
df = pd.read_parquet(f'https://
|
184
|
-
goalie_df = pd.read_parquet(f'https://
|
185
|
-
pbp = pd.read_parquet(f'https://
|
184
|
+
df = pd.read_parquet(f'https://weakside-breakout.s3.us-east-2.amazonaws.com/game_log/wsba_nhl_{season}_game_log.parquet')
|
185
|
+
goalie_df = pd.read_parquet(f'https://weakside-breakout.s3.us-east-2.amazonaws.com/game_log/goalie/wsba_nhl_{season}_game_log_goalie.parquet')
|
186
|
+
pbp = pd.read_parquet(f'https://weakside-breakout.s3.us-east-2.amazonaws.com/pbp/{season}.parquet')
|
186
187
|
|
187
188
|
game_df.set([df.loc[(df['Game']==info['game_id'])], pbp.loc[(pbp['game_id']==info['game_id'])&(pbp['event_type']=='goal')], goalie_df.loc[(goalie_df['Game']==info['game_id'])]])
|
188
189
|
|
@@ -247,7 +248,7 @@ def server(input, output, session):
|
|
247
248
|
@output
|
248
249
|
@render.text
|
249
250
|
def game_header():
|
250
|
-
return game_info.get()['title']
|
251
|
+
return game_info.get()['title'] if active_params()['title'][0] == 'true' else None
|
251
252
|
|
252
253
|
@output
|
253
254
|
@render.text
|
@@ -0,0 +1,101 @@
|
|
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_goalie"),
|
14
|
+
)
|
15
|
+
|
16
|
+
def server(input, output, session):
|
17
|
+
@output()
|
18
|
+
@render_widget
|
19
|
+
def plot_goalie():
|
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 goalie and plot all 5v5 fenwick shots
|
26
|
+
defaults = {
|
27
|
+
'goalie':['8471679'],
|
28
|
+
'season':['20142015'],
|
29
|
+
'team':['MTL'],
|
30
|
+
'strength_state':['5v5'],
|
31
|
+
'season_type':['2']
|
32
|
+
}
|
33
|
+
|
34
|
+
for key in defaults.keys():
|
35
|
+
if key not in query.keys():
|
36
|
+
query.update({key:defaults[key]})
|
37
|
+
|
38
|
+
#Iterate through query and parse params with multiple selections
|
39
|
+
for param in query.keys():
|
40
|
+
q_string = query[param][0]
|
41
|
+
query[param] = q_string.split(',')
|
42
|
+
|
43
|
+
print(query)
|
44
|
+
#Determine which season to load based on the input
|
45
|
+
season = query['season'][0]
|
46
|
+
#Load appropriate dataframe
|
47
|
+
df = pd.read_parquet(f'https://weakside-breakout.s3.us-east-2.amazonaws.com/pbp/{season}.parquet')
|
48
|
+
|
49
|
+
#Prepare dataframe for plotting based on URL parameters
|
50
|
+
df = df.loc[(df['season'].astype(str).isin(query['season']))&(df['season_type'].astype(str).isin(query['season_type']))].replace({np.nan: None})
|
51
|
+
#Return empty rink if no data exists else continue
|
52
|
+
if df.empty:
|
53
|
+
return wsba_plt.wsba_rink(setting='offense',vertical=True)
|
54
|
+
else:
|
55
|
+
rink = wsba_plt.wsba_rink(setting='offense',vertical=True)
|
56
|
+
|
57
|
+
try:
|
58
|
+
plot = wsba_plt.heatmap(df,goalie=query['goalie'][0],team=query['team'][0],events=['missed-shot','shot-on-goal','goal'],strengths=query['strength_state'])
|
59
|
+
|
60
|
+
for trace in plot.data:
|
61
|
+
rink.add_trace(trace)
|
62
|
+
|
63
|
+
player = query['goalie'][0]
|
64
|
+
season = int(season[0:4])
|
65
|
+
team = query['team'][0]
|
66
|
+
strengths = 'All Situations' if len(query['strength_state']) == 4 else query['strength_state']
|
67
|
+
span = 'Regular Season' if query['season_type'][0]=='2' else 'Playoffs'
|
68
|
+
|
69
|
+
return rink.update_layout(
|
70
|
+
title=dict(
|
71
|
+
text=f'{player} GSAx at {strengths}; {season}-{season+1}, {span}, {team}',
|
72
|
+
x=0.5, y=0.96,
|
73
|
+
xanchor='center',
|
74
|
+
yanchor='top',
|
75
|
+
font=dict(color='white')
|
76
|
+
),
|
77
|
+
).add_annotation(
|
78
|
+
text='Lower GSAx',
|
79
|
+
xref="paper",
|
80
|
+
yref="paper",
|
81
|
+
xanchor='right',
|
82
|
+
yanchor='top',
|
83
|
+
font=dict(color='white'),
|
84
|
+
x=0.3,
|
85
|
+
y=0.04,
|
86
|
+
showarrow=False
|
87
|
+
).add_annotation(
|
88
|
+
text='Higher GSAx',
|
89
|
+
xref="paper",
|
90
|
+
yref="paper",
|
91
|
+
xanchor='right',
|
92
|
+
yanchor='top',
|
93
|
+
font=dict(color='white'),
|
94
|
+
x=0.76,
|
95
|
+
y=0.04,
|
96
|
+
showarrow=False
|
97
|
+
)
|
98
|
+
except:
|
99
|
+
return wsba_plt.wsba_rink(setting='offense',vertical=True)
|
100
|
+
|
101
|
+
app = App(app_ui, server)
|
@@ -0,0 +1,71 @@
|
|
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,goalie,team,events,strengths):
|
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.loc[(df['event_type'].isin(events))&(df['x_adj'].notna())&(df['y_adj'].notna())]
|
17
|
+
df['x'] = np.where(df['x_adj']<0,-df['y_adj'],df['y_adj'])
|
18
|
+
df['y'] = abs(df['x_adj'])
|
19
|
+
df['event_distance'] = abs(df['event_distance'].fillna(0))
|
20
|
+
df = df.loc[(df['event_distance']<=89)&(df['y']<=89)&(df['empty_net']==0)]
|
21
|
+
|
22
|
+
y_min = 0
|
23
|
+
y_max = 100
|
24
|
+
|
25
|
+
df['G'] = (df['event_type']=='goal').astype(int)
|
26
|
+
df['strength_state_2'] = np.where(df['strength_state_2'].isin(['5v5','5v4','4v5']),df['strength_state_2'],'Other')
|
27
|
+
|
28
|
+
if strengths != 'all':
|
29
|
+
df = df.loc[((df['strength_state_2'].isin(strengths)))]
|
30
|
+
|
31
|
+
[x,y] = np.round(np.meshgrid(np.linspace(-42.5,42.5,85),np.linspace(y_min,y_max,(y_max-y_min))))
|
32
|
+
xgoals = griddata((df['x'],df['y']),df['xG']-df['G'],(x,y),method='cubic',fill_value=0)
|
33
|
+
xgoals_smooth = gaussian_filter(xgoals,sigma=3)
|
34
|
+
|
35
|
+
player_shots = df.loc[(df['event_goalie_id'].astype(str).str.contains(goalie))&(df['event_team_abbr_2']==team)]
|
36
|
+
[x,y] = np.round(np.meshgrid(np.linspace(-42.5,42.5,85),np.linspace(y_min,y_max,(y_max-y_min))))
|
37
|
+
xgoals_player = griddata((player_shots['x'],player_shots['y']),player_shots['xG']-player_shots['G'],(x,y),method='cubic',fill_value=0)
|
38
|
+
|
39
|
+
difference = (gaussian_filter(xgoals_player,sigma = 3)) - xgoals_smooth
|
40
|
+
data_min= difference.min()
|
41
|
+
data_max= difference.max()
|
42
|
+
|
43
|
+
if abs(data_min) > data_max:
|
44
|
+
data_max = data_min * -1
|
45
|
+
elif data_max > abs(data_min):
|
46
|
+
data_min = data_max * -1
|
47
|
+
|
48
|
+
fig = go.Figure(
|
49
|
+
data = go.Contour( x=np.linspace(-42.5,42,5,85),
|
50
|
+
y=np.linspace(y_min,y_max,(y_max-y_min)),
|
51
|
+
z=xgoals_smooth,
|
52
|
+
colorscale=[[0.0,'red'],[0.5,'#09090b'],[1.0,'blue']],
|
53
|
+
connectgaps=True,
|
54
|
+
contours=dict(
|
55
|
+
type='levels',
|
56
|
+
start = data_min,
|
57
|
+
end = data_max,
|
58
|
+
size=(data_max-data_min)/11
|
59
|
+
),
|
60
|
+
colorbar=dict(
|
61
|
+
len = 0.7,
|
62
|
+
orientation='h',
|
63
|
+
showticklabels=False,
|
64
|
+
thickness=15,
|
65
|
+
yref='paper',
|
66
|
+
yanchor='top',
|
67
|
+
y=0
|
68
|
+
))
|
69
|
+
)
|
70
|
+
|
71
|
+
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://f005.backblazeb2.com/file/weakside-breakout/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
|
@@ -44,7 +44,7 @@ def server(input, output, session):
|
|
44
44
|
#Determine which season to load based on the input
|
45
45
|
season = query['season'][0]
|
46
46
|
#Load appropriate dataframe
|
47
|
-
df = pd.read_parquet(f'https://
|
47
|
+
df = pd.read_parquet(f'https://weakside-breakout.s3.us-east-2.amazonaws.com/pbp/{season}.parquet')
|
48
48
|
|
49
49
|
#Prepare dataframe for plotting based on URL parameters
|
50
50
|
df = df.loc[(df['season'].astype(str).isin(query['season']))&(df['season_type'].astype(str).isin(query['season_type']))].replace({np.nan: None})
|
@@ -13,6 +13,7 @@ def heatmap(df,skater,team,events,strengths,onice):
|
|
13
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
14
|
df['strength_state_2'] = df['strength_state'].str[::-1]
|
15
15
|
|
16
|
+
df = df.fillna(0)
|
16
17
|
df = df.loc[(df['event_type'].isin(events))&(df['x_adj'].notna())&(df['y_adj'].notna())]
|
17
18
|
if onice == 'for':
|
18
19
|
df['x'] = abs(df['x_adj'])
|
@@ -38,6 +39,7 @@ def heatmap(df,skater,team,events,strengths,onice):
|
|
38
39
|
df['onice_against'] = np.where(df['away_team_abbr']==df['event_team_abbr'],df['home_on_ice'],df['away_on_ice'])
|
39
40
|
|
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')
|
41
43
|
|
42
44
|
if strengths != 'all':
|
43
45
|
if onice == 'against':
|
@@ -222,7 +222,7 @@ def rink(setting = "full", vertical = False):
|
|
222
222
|
)
|
223
223
|
|
224
224
|
# Add logo
|
225
|
-
logo = Image.open(rs.get('https://
|
225
|
+
logo = Image.open(rs.get('https://weakside-breakout.s3.us-east-2.amazonaws.com/utils/wsba.png',stream=True).raw)
|
226
226
|
|
227
227
|
fig.add_layout_image(
|
228
228
|
dict(
|
@@ -50,12 +50,12 @@ def server(input, output, session):
|
|
50
50
|
season_2 = query['season_2'][0]
|
51
51
|
#Load appropriate dataframes
|
52
52
|
if season_1 == season_2:
|
53
|
-
df_1 = pd.read_parquet(f'https://
|
53
|
+
df_1 = pd.read_parquet(f'https://weakside-breakout.s3.us-east-2.amazonaws.com/pbp/{season_1}.parquet')
|
54
54
|
df_2 = df_1
|
55
55
|
|
56
56
|
else:
|
57
|
-
df_1 = pd.read_parquet(f'https://
|
58
|
-
df_2 = pd.read_parquet(f'https://
|
57
|
+
df_1 = pd.read_parquet(f'https://weakside-breakout.s3.us-east-2.amazonaws.com/pbp/{season_1}.parquet')
|
58
|
+
df_2 = pd.read_parquet(f'https://weakside-breakout.s3.us-east-2.amazonaws.com/pbp/{season_2}.parquet')
|
59
59
|
|
60
60
|
events = ['missed-shot','shot-on-goal','goal']
|
61
61
|
team_xg = {}
|
@@ -12,6 +12,7 @@ def wsba_rink(setting='full', vertical=False):
|
|
12
12
|
def heatmap_prep(df,team,events,strengths,onice,flip=False):
|
13
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
14
|
|
15
|
+
df = df.fillna(0)
|
15
16
|
df = df.loc[(df['event_type'].isin(events))&(df['x_adj'].notna())&(df['y_adj'].notna())]
|
16
17
|
if flip:
|
17
18
|
if onice == 'for':
|
@@ -55,6 +56,7 @@ def heatmap_prep(df,team,events,strengths,onice,flip=False):
|
|
55
56
|
df['onice_against'] = np.where(df['away_team_abbr']==df['event_team_abbr'],df['home_on_ice'],df['away_on_ice'])
|
56
57
|
|
57
58
|
df['strength_state'] = np.where(df['strength_state'].isin(['5v5','5v4','4v5']),df['strength_state'],'Other')
|
59
|
+
df['strength_state_2'] = np.where(df['strength_state_2'].isin(['5v5','5v4','4v5']),df['strength_state_2'],'Other')
|
58
60
|
|
59
61
|
if strengths != 'all':
|
60
62
|
df = df.loc[((df['strength_state'].isin(strengths)))]
|
@@ -222,7 +222,7 @@ def rink(setting = "full", vertical = False):
|
|
222
222
|
)
|
223
223
|
|
224
224
|
# Add logo
|
225
|
-
logo = Image.open(rs.get('https://
|
225
|
+
logo = Image.open(rs.get('https://weakside-breakout.s3.us-east-2.amazonaws.com/utils/wsba.png',stream=True).raw)
|
226
226
|
|
227
227
|
fig.add_layout_image(
|
228
228
|
dict(
|