hockey-blast-common-lib 0.1.28__py3-none-any.whl → 0.1.31__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.
- hockey_blast_common_lib/aggregate_skater_stats.py +87 -113
- hockey_blast_common_lib/assign_skater_skill.py +46 -0
- hockey_blast_common_lib/hockey_blast_sample_backup.sql.gz +0 -0
- hockey_blast_common_lib/models.py +5 -5
- hockey_blast_common_lib/options.py +1 -0
- hockey_blast_common_lib/skills_in_divisions.py +21 -59
- hockey_blast_common_lib/skills_propagation.py +368 -0
- hockey_blast_common_lib/stats_models.py +51 -0
- {hockey_blast_common_lib-0.1.28.dist-info → hockey_blast_common_lib-0.1.31.dist-info}/METADATA +1 -1
- hockey_blast_common_lib-0.1.31.dist-info/RECORD +21 -0
- hockey_blast_common_lib-0.1.28.dist-info/RECORD +0 -19
- {hockey_blast_common_lib-0.1.28.dist-info → hockey_blast_common_lib-0.1.31.dist-info}/WHEEL +0 -0
- {hockey_blast_common_lib-0.1.28.dist-info → hockey_blast_common_lib-0.1.31.dist-info}/top_level.txt +0 -0
@@ -6,17 +6,28 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
6
6
|
from datetime import datetime, timedelta
|
7
7
|
import sqlalchemy
|
8
8
|
|
9
|
-
from hockey_blast_common_lib.models import Game, Goal, Penalty, GameRoster
|
10
|
-
from hockey_blast_common_lib.stats_models import OrgStatsSkater, DivisionStatsSkater, OrgStatsWeeklySkater, OrgStatsDailySkater, DivisionStatsWeeklySkater, DivisionStatsDailySkater
|
9
|
+
from hockey_blast_common_lib.models import Game, Goal, Penalty, GameRoster, Organization, Division, Human, Level
|
10
|
+
from hockey_blast_common_lib.stats_models import OrgStatsSkater, DivisionStatsSkater, OrgStatsWeeklySkater, OrgStatsDailySkater, DivisionStatsWeeklySkater, DivisionStatsDailySkater, LevelStatsSkater
|
11
11
|
from hockey_blast_common_lib.db_connection import create_session
|
12
12
|
from sqlalchemy.sql import func, case
|
13
|
-
from hockey_blast_common_lib.options import not_human_names, parse_args, MIN_GAMES_FOR_ORG_STATS, MIN_GAMES_FOR_DIVISION_STATS
|
14
|
-
from hockey_blast_common_lib.utils import get_org_id_from_alias, get_human_ids_by_names, get_division_ids_for_last_season_in_all_leagues
|
13
|
+
from hockey_blast_common_lib.options import not_human_names, parse_args, MIN_GAMES_FOR_ORG_STATS, MIN_GAMES_FOR_DIVISION_STATS, MIN_GAMES_FOR_LEVEL_STATS
|
14
|
+
from hockey_blast_common_lib.utils import get_org_id_from_alias, get_human_ids_by_names, get_division_ids_for_last_season_in_all_leagues, get_all_division_ids_for_org
|
15
15
|
from sqlalchemy import func, case, and_
|
16
|
+
from collections import defaultdict
|
16
17
|
|
17
|
-
def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_filter_out,
|
18
|
+
def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_filter_out, debug_human_id=None, aggregation_window=None):
|
18
19
|
human_ids_to_filter = get_human_ids_by_names(session, names_to_filter_out)
|
19
20
|
|
21
|
+
# Get the name of the aggregation, for debug purposes
|
22
|
+
if aggregation_type == 'org':
|
23
|
+
aggregation_name = session.query(Organization).filter(Organization.id == aggregation_id).first().organization_name
|
24
|
+
elif aggregation_type == 'division':
|
25
|
+
aggregation_name = session.query(Division).filter(Division.id == aggregation_id).first().level
|
26
|
+
elif aggregation_type == 'level':
|
27
|
+
aggregation_name = session.query(Level).filter(Level.id == aggregation_id).first().level_name
|
28
|
+
else:
|
29
|
+
aggregation_name = "Unknown"
|
30
|
+
|
20
31
|
if aggregation_type == 'org':
|
21
32
|
if aggregation_window == 'Daily':
|
22
33
|
StatsModel = OrgStatsDailySkater
|
@@ -35,6 +46,14 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
|
|
35
46
|
StatsModel = DivisionStatsSkater
|
36
47
|
min_games = MIN_GAMES_FOR_DIVISION_STATS
|
37
48
|
filter_condition = Game.division_id == aggregation_id
|
49
|
+
elif aggregation_type == 'level':
|
50
|
+
StatsModel = LevelStatsSkater
|
51
|
+
min_games = MIN_GAMES_FOR_LEVEL_STATS
|
52
|
+
filter_condition = Division.level_id == aggregation_id
|
53
|
+
# Add filter to only include games for the last 5 years
|
54
|
+
# five_years_ago = datetime.now() - timedelta(days=5*365)
|
55
|
+
# level_window_filter = func.cast(func.concat(Game.date, ' ', Game.time), sqlalchemy.types.TIMESTAMP) >= five_years_ago
|
56
|
+
# filter_condition = filter_condition & level_window_filter
|
38
57
|
else:
|
39
58
|
raise ValueError("Invalid aggregation type")
|
40
59
|
|
@@ -59,8 +78,8 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
|
|
59
78
|
|
60
79
|
# Filter for specific human_id if provided
|
61
80
|
human_filter = []
|
62
|
-
if
|
63
|
-
|
81
|
+
# if debug_human_id:
|
82
|
+
# human_filter = [GameRoster.human_id == debug_human_id]
|
64
83
|
|
65
84
|
# Aggregate games played for each human in each division, excluding goalies
|
66
85
|
games_played_stats = session.query(
|
@@ -68,7 +87,7 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
|
|
68
87
|
GameRoster.human_id,
|
69
88
|
func.count(Game.id).label('games_played'),
|
70
89
|
func.array_agg(Game.id).label('game_ids')
|
71
|
-
).join(GameRoster, Game.id == GameRoster.game_id).filter(filter_condition, ~GameRoster.role.ilike('g'), *human_filter).group_by(Game.org_id, GameRoster.human_id).all()
|
90
|
+
).join(GameRoster, Game.id == GameRoster.game_id).join(Division, Game.division_id == Division.id).filter(filter_condition, ~GameRoster.role.ilike('g'), *human_filter).group_by(Game.org_id, GameRoster.human_id).all()
|
72
91
|
|
73
92
|
# Aggregate goals for each human in each division, excluding goalies
|
74
93
|
goals_stats = session.query(
|
@@ -76,7 +95,7 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
|
|
76
95
|
Goal.goal_scorer_id.label('human_id'),
|
77
96
|
func.count(Goal.id).label('goals'),
|
78
97
|
func.array_agg(Goal.game_id).label('goal_game_ids')
|
79
|
-
).join(Game, Game.id == Goal.game_id).join(GameRoster, and_(Game.id == GameRoster.game_id, Goal.goal_scorer_id == GameRoster.human_id)).filter(filter_condition, ~GameRoster.role.ilike('g'), *human_filter).group_by(Game.org_id, Goal.goal_scorer_id).all()
|
98
|
+
).join(Game, Game.id == Goal.game_id).join(GameRoster, and_(Game.id == GameRoster.game_id, Goal.goal_scorer_id == GameRoster.human_id)).join(Division, Game.division_id == Division.id).filter(filter_condition, ~GameRoster.role.ilike('g'), *human_filter).group_by(Game.org_id, Goal.goal_scorer_id).all()
|
80
99
|
|
81
100
|
# Aggregate assists for each human in each division, excluding goalies
|
82
101
|
assists_stats = session.query(
|
@@ -84,14 +103,14 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
|
|
84
103
|
Goal.assist_1_id.label('human_id'),
|
85
104
|
func.count(Goal.id).label('assists'),
|
86
105
|
func.array_agg(Goal.game_id).label('assist_game_ids')
|
87
|
-
).join(Game, Game.id == Goal.game_id).join(GameRoster, and_(Game.id == GameRoster.game_id, Goal.assist_1_id == GameRoster.human_id)).filter(filter_condition, ~GameRoster.role.ilike('g'), *human_filter).group_by(Game.org_id, Goal.assist_1_id).all()
|
106
|
+
).join(Game, Game.id == Goal.game_id).join(GameRoster, and_(Game.id == GameRoster.game_id, Goal.assist_1_id == GameRoster.human_id)).join(Division, Game.division_id == Division.id).filter(filter_condition, ~GameRoster.role.ilike('g'), *human_filter).group_by(Game.org_id, Goal.assist_1_id).all()
|
88
107
|
|
89
108
|
assists_stats_2 = session.query(
|
90
109
|
Game.org_id,
|
91
110
|
Goal.assist_2_id.label('human_id'),
|
92
111
|
func.count(Goal.id).label('assists'),
|
93
112
|
func.array_agg(Goal.game_id).label('assist_2_game_ids')
|
94
|
-
).join(Game, Game.id == Goal.game_id).join(GameRoster, and_(Game.id == GameRoster.game_id, Goal.assist_2_id == GameRoster.human_id)).filter(filter_condition, ~GameRoster.role.ilike('g'), *human_filter).group_by(Game.org_id, Goal.assist_2_id).all()
|
113
|
+
).join(Game, Game.id == Goal.game_id).join(GameRoster, and_(Game.id == GameRoster.game_id, Goal.assist_2_id == GameRoster.human_id)).join(Division, Game.division_id == Division.id).filter(filter_condition, ~GameRoster.role.ilike('g'), *human_filter).group_by(Game.org_id, Goal.assist_2_id).all()
|
95
114
|
|
96
115
|
# Aggregate penalties for each human in each division, excluding goalies
|
97
116
|
penalties_stats = session.query(
|
@@ -99,7 +118,7 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
|
|
99
118
|
Penalty.penalized_player_id.label('human_id'),
|
100
119
|
func.count(Penalty.id).label('penalties'),
|
101
120
|
func.array_agg(Penalty.game_id).label('penalty_game_ids')
|
102
|
-
).join(Game, Game.id == Penalty.game_id).join(GameRoster, and_(Game.id == GameRoster.game_id, Penalty.penalized_player_id == GameRoster.human_id)).filter(filter_condition, ~GameRoster.role.ilike('g'), *human_filter).group_by(Game.org_id, Penalty.penalized_player_id).all()
|
121
|
+
).join(Game, Game.id == Penalty.game_id).join(GameRoster, and_(Game.id == GameRoster.game_id, Penalty.penalized_player_id == GameRoster.human_id)).join(Division, Game.division_id == Division.id).filter(filter_condition, ~GameRoster.role.ilike('g'), *human_filter).group_by(Game.org_id, Penalty.penalized_player_id).all()
|
103
122
|
|
104
123
|
# Combine the results
|
105
124
|
stats_dict = {}
|
@@ -107,6 +126,8 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
|
|
107
126
|
if stat.human_id in human_ids_to_filter:
|
108
127
|
continue
|
109
128
|
key = (aggregation_id, stat.human_id)
|
129
|
+
if stat.games_played < min_games:
|
130
|
+
continue
|
110
131
|
stats_dict[key] = {
|
111
132
|
'games_played': stat.games_played,
|
112
133
|
'goals': 0,
|
@@ -123,94 +144,26 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
|
|
123
144
|
}
|
124
145
|
|
125
146
|
for stat in goals_stats:
|
126
|
-
if stat.human_id in human_ids_to_filter:
|
127
|
-
continue
|
128
147
|
key = (aggregation_id, stat.human_id)
|
129
|
-
if key
|
130
|
-
stats_dict[key] = {
|
131
|
-
'games_played': 0,
|
132
|
-
'goals': stat.goals,
|
133
|
-
'assists': 0,
|
134
|
-
'penalties': 0,
|
135
|
-
'points': stat.goals, # Initialize points with goals
|
136
|
-
'goals_per_game': 0.0,
|
137
|
-
'points_per_game': 0.0,
|
138
|
-
'assists_per_game': 0.0,
|
139
|
-
'penalties_per_game': 0.0,
|
140
|
-
'game_ids': [],
|
141
|
-
'first_game_id': None,
|
142
|
-
'last_game_id': None
|
143
|
-
}
|
144
|
-
else:
|
148
|
+
if key in stats_dict:
|
145
149
|
stats_dict[key]['goals'] += stat.goals
|
146
150
|
stats_dict[key]['points'] += stat.goals # Update points
|
147
151
|
|
148
152
|
for stat in assists_stats:
|
149
|
-
if stat.human_id in human_ids_to_filter:
|
150
|
-
continue
|
151
153
|
key = (aggregation_id, stat.human_id)
|
152
|
-
if key
|
153
|
-
stats_dict[key] = {
|
154
|
-
'games_played': 0,
|
155
|
-
'goals': 0,
|
156
|
-
'assists': stat.assists,
|
157
|
-
'penalties': 0,
|
158
|
-
'points': stat.assists, # Initialize points with assists
|
159
|
-
'goals_per_game': 0.0,
|
160
|
-
'points_per_game': 0.0,
|
161
|
-
'assists_per_game': 0.0,
|
162
|
-
'penalties_per_game': 0.0,
|
163
|
-
'game_ids': [],
|
164
|
-
'first_game_id': None,
|
165
|
-
'last_game_id': None
|
166
|
-
}
|
167
|
-
else:
|
154
|
+
if key in stats_dict:
|
168
155
|
stats_dict[key]['assists'] += stat.assists
|
169
156
|
stats_dict[key]['points'] += stat.assists # Update points
|
170
157
|
|
171
158
|
for stat in assists_stats_2:
|
172
|
-
if stat.human_id in human_ids_to_filter:
|
173
|
-
continue
|
174
159
|
key = (aggregation_id, stat.human_id)
|
175
|
-
if key
|
176
|
-
stats_dict[key] = {
|
177
|
-
'games_played': 0,
|
178
|
-
'goals': 0,
|
179
|
-
'assists': stat.assists,
|
180
|
-
'penalties': 0,
|
181
|
-
'points': stat.assists, # Initialize points with assists
|
182
|
-
'goals_per_game': 0.0,
|
183
|
-
'points_per_game': 0.0,
|
184
|
-
'assists_per_game': 0.0,
|
185
|
-
'penalties_per_game': 0.0,
|
186
|
-
'game_ids': [],
|
187
|
-
'first_game_id': None,
|
188
|
-
'last_game_id': None
|
189
|
-
}
|
190
|
-
else:
|
160
|
+
if key in stats_dict:
|
191
161
|
stats_dict[key]['assists'] += stat.assists
|
192
162
|
stats_dict[key]['points'] += stat.assists # Update points
|
193
163
|
|
194
164
|
for stat in penalties_stats:
|
195
|
-
if stat.human_id in human_ids_to_filter:
|
196
|
-
continue
|
197
165
|
key = (aggregation_id, stat.human_id)
|
198
|
-
if key
|
199
|
-
stats_dict[key] = {
|
200
|
-
'games_played': 0,
|
201
|
-
'goals': 0,
|
202
|
-
'assists': 0,
|
203
|
-
'penalties': stat.penalties,
|
204
|
-
'points': 0, # Initialize points
|
205
|
-
'goals_per_game': 0.0,
|
206
|
-
'points_per_game': 0.0,
|
207
|
-
'assists_per_game': 0.0,
|
208
|
-
'penalties_per_game': 0.0,
|
209
|
-
'game_ids': [],
|
210
|
-
'first_game_id': None,
|
211
|
-
'last_game_id': None
|
212
|
-
}
|
213
|
-
else:
|
166
|
+
if key in stats_dict:
|
214
167
|
stats_dict[key]['penalties'] += stat.penalties
|
215
168
|
|
216
169
|
# Calculate per game stats
|
@@ -233,20 +186,10 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
|
|
233
186
|
stat['first_game_id'] = first_game.id if first_game else None
|
234
187
|
stat['last_game_id'] = last_game.id if last_game else None
|
235
188
|
|
236
|
-
# Debug output for totals if filter_human_id is provided
|
237
|
-
if filter_human_id:
|
238
|
-
for key, stat in stats_dict.items():
|
239
|
-
if key[1] == filter_human_id:
|
240
|
-
print(f"Human ID: {filter_human_id}")
|
241
|
-
print(f"Total Games Played: {stat['games_played']}")
|
242
|
-
print(f"Total Goals: {stat['goals']}")
|
243
|
-
print(f"Total Assists: {stat['assists']}")
|
244
|
-
print(f"Total Penalties: {stat['penalties']}")
|
245
|
-
|
246
189
|
# Calculate total_in_rank
|
247
190
|
total_in_rank = len(stats_dict)
|
248
191
|
|
249
|
-
# Assign ranks
|
192
|
+
# Assign ranks within each level
|
250
193
|
def assign_ranks(stats_dict, field):
|
251
194
|
sorted_stats = sorted(stats_dict.items(), key=lambda x: x[1][field], reverse=True)
|
252
195
|
for rank, (key, stat) in enumerate(sorted_stats, start=1):
|
@@ -262,13 +205,22 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
|
|
262
205
|
assign_ranks(stats_dict, 'assists_per_game')
|
263
206
|
assign_ranks(stats_dict, 'penalties_per_game')
|
264
207
|
|
208
|
+
# Debug output for specific human
|
209
|
+
if debug_human_id:
|
210
|
+
if any(key[1] == debug_human_id for key in stats_dict):
|
211
|
+
human = session.query(Human).filter(Human.id == debug_human_id).first()
|
212
|
+
human_name = f"{human.first_name} {human.last_name}" if human else "Unknown"
|
213
|
+
print(f"For Human {debug_human_id} ({human_name}) for {aggregation_type} {aggregation_id} ({aggregation_name}) , total_in_rank {total_in_rank} and window {aggregation_window}:")
|
214
|
+
for key, stat in stats_dict.items():
|
215
|
+
if key[1] == debug_human_id:
|
216
|
+
for k, v in stat.items():
|
217
|
+
print(f"{k}: {v}")
|
218
|
+
|
265
219
|
# Insert aggregated stats into the appropriate table with progress output
|
266
220
|
total_items = len(stats_dict)
|
267
221
|
batch_size = 1000
|
268
222
|
for i, (key, stat) in enumerate(stats_dict.items(), 1):
|
269
223
|
aggregation_id, human_id = key
|
270
|
-
if stat['games_played'] < min_games:
|
271
|
-
continue
|
272
224
|
goals_per_game = stat['goals'] / stat['games_played'] if stat['games_played'] > 0 else 0.0
|
273
225
|
points_per_game = (stat['goals'] + stat['assists']) / stat['games_played'] if stat['games_played'] > 0 else 0.0
|
274
226
|
assists_per_game = stat['assists'] / stat['games_played'] if stat['games_played'] > 0 else 0.0
|
@@ -302,25 +254,47 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
|
|
302
254
|
# Commit in batches
|
303
255
|
if i % batch_size == 0:
|
304
256
|
session.commit()
|
305
|
-
|
257
|
+
if debug_human_id is None:
|
258
|
+
print(f"\r{i}/{total_items} ({(i/total_items)*100:.2f}%)", end="")
|
259
|
+
|
306
260
|
session.commit()
|
307
|
-
|
308
|
-
|
261
|
+
if debug_human_id is None:
|
262
|
+
print(f"\r{total_items}/{total_items} (100.00%)")
|
309
263
|
|
310
|
-
# Example usage
|
311
264
|
if __name__ == "__main__":
|
312
|
-
args = parse_args()
|
313
|
-
org_alias = args.org
|
314
265
|
session = create_session("boss")
|
315
|
-
|
266
|
+
human_id_to_debug = 117076
|
316
267
|
|
317
|
-
|
318
|
-
|
319
|
-
for
|
320
|
-
aggregate_skater_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, filter_human_id=None)
|
321
|
-
aggregate_skater_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, filter_human_id=None, aggregation_window='Weekly')
|
322
|
-
aggregate_skater_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, filter_human_id=None, aggregation_window='Daily')
|
268
|
+
# Get all org_id present in the Organization table
|
269
|
+
org_ids = session.query(Organization.id).all()
|
270
|
+
org_ids = [org_id[0] for org_id in org_ids]
|
323
271
|
|
324
|
-
|
325
|
-
|
326
|
-
|
272
|
+
for org_id in org_ids:
|
273
|
+
division_ids = get_all_division_ids_for_org(session, org_id)
|
274
|
+
print(f"Aggregating skater stats for {len(division_ids)} divisions in org_id {org_id}...")
|
275
|
+
total_divisions = len(division_ids)
|
276
|
+
processed_divisions = 0
|
277
|
+
for division_id in division_ids:
|
278
|
+
aggregate_skater_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, debug_human_id=human_id_to_debug)
|
279
|
+
aggregate_skater_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, debug_human_id=human_id_to_debug, aggregation_window='Weekly')
|
280
|
+
aggregate_skater_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, debug_human_id=human_id_to_debug, aggregation_window='Daily')
|
281
|
+
processed_divisions += 1
|
282
|
+
if human_id_to_debug is None:
|
283
|
+
print(f"\rProcessed {processed_divisions}/{total_divisions} divisions ({(processed_divisions/total_divisions)*100:.2f}%)", end="")
|
284
|
+
|
285
|
+
aggregate_skater_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names, debug_human_id=human_id_to_debug)
|
286
|
+
aggregate_skater_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names, debug_human_id=human_id_to_debug, aggregation_window='Weekly')
|
287
|
+
aggregate_skater_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names, debug_human_id=human_id_to_debug, aggregation_window='Daily')
|
288
|
+
|
289
|
+
# Aggregate by level
|
290
|
+
level_ids = session.query(Division.level_id).distinct().all()
|
291
|
+
level_ids = [level_id[0] for level_id in level_ids]
|
292
|
+
total_levels = len(level_ids)
|
293
|
+
processed_levels = 0
|
294
|
+
for level_id in level_ids:
|
295
|
+
if level_id is None:
|
296
|
+
continue
|
297
|
+
if human_id_to_debug is None:
|
298
|
+
print(f"\rProcessed {processed_levels}/{total_levels} levels ({(processed_levels/total_levels)*100:.2f}%)", end="")
|
299
|
+
processed_levels += 1
|
300
|
+
aggregate_skater_stats(session, aggregation_type='level', aggregation_id=level_id, names_to_filter_out=not_human_names, debug_human_id=human_id_to_debug)
|
@@ -0,0 +1,46 @@
|
|
1
|
+
import sys, os
|
2
|
+
|
3
|
+
# Add the package directory to the Python path
|
4
|
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
5
|
+
|
6
|
+
from hockey_blast_common_lib.models import Human, LevelStatsSkater
|
7
|
+
from hockey_blast_common_lib.db_connection import create_session
|
8
|
+
from sqlalchemy.sql import func
|
9
|
+
|
10
|
+
def calculate_skater_skill_value(human_id, level_stats):
|
11
|
+
max_skill_value = 0
|
12
|
+
|
13
|
+
for stat in level_stats:
|
14
|
+
level_skill_value = stat.level.skill_value
|
15
|
+
if level_skill_value < 0:
|
16
|
+
continue
|
17
|
+
ppg_ratio = stat.points_per_game_rank / stat.total_in_rank
|
18
|
+
games_played_ratio = stat.games_played_rank / stat.total_in_rank
|
19
|
+
|
20
|
+
# Take the maximum of the two ratios
|
21
|
+
skill_value = level_skill_value * max(ppg_ratio, games_played_ratio)
|
22
|
+
max_skill_value = max(max_skill_value, skill_value)
|
23
|
+
|
24
|
+
return max_skill_value
|
25
|
+
|
26
|
+
def assign_skater_skill_values():
|
27
|
+
session = create_session("boss")
|
28
|
+
|
29
|
+
humans = session.query(Human).all()
|
30
|
+
total_humans = len(humans)
|
31
|
+
processed_humans = 0
|
32
|
+
|
33
|
+
for human in humans:
|
34
|
+
level_stats = session.query(LevelStatsSkater).filter(LevelStatsSkater.human_id == human.id).all()
|
35
|
+
if level_stats:
|
36
|
+
skater_skill_value = calculate_skater_skill_value(human.id, level_stats)
|
37
|
+
human.skater_skill_value = skater_skill_value
|
38
|
+
session.commit()
|
39
|
+
|
40
|
+
processed_humans += 1
|
41
|
+
print(f"\rProcessed {processed_humans}/{total_humans} humans ({(processed_humans/total_humans)*100:.2f}%)", end="")
|
42
|
+
|
43
|
+
print("\nSkater skill values have been assigned to all humans.")
|
44
|
+
|
45
|
+
if __name__ == "__main__":
|
46
|
+
assign_skater_skill_values()
|
Binary file
|
@@ -18,9 +18,8 @@ class Division(db.Model):
|
|
18
18
|
league_number = db.Column(db.Integer) # TODO: Deprecate usage and remove (get this info through Season->League)
|
19
19
|
season_number = db.Column(db.Integer) # TODO: Deprecate usage and remove (get this info from Season by season_id)
|
20
20
|
season_id = db.Column(db.Integer, db.ForeignKey('seasons.id'))
|
21
|
-
level = db.Column(db.String(100))
|
22
|
-
|
23
|
-
skill_propagation_sequence = db.Column(db.Integer, nullable=True, default=-1)
|
21
|
+
level = db.Column(db.String(100)) # Obsolete, use skill_id instead
|
22
|
+
level_id = db.Column(db.Integer, db.ForeignKey('levels.id')) # New field
|
24
23
|
org_id = db.Column(db.Integer, db.ForeignKey('organizations.id'), nullable=False)
|
25
24
|
__table_args__ = (
|
26
25
|
db.UniqueConstraint('org_id', 'league_number', 'season_number', 'level', name='_org_league_season_level_uc'),
|
@@ -154,14 +153,15 @@ class League(db.Model):
|
|
154
153
|
db.UniqueConstraint('org_id', 'league_number', name='_org_league_number_uc'),
|
155
154
|
)
|
156
155
|
|
157
|
-
class
|
158
|
-
__tablename__ = '
|
156
|
+
class Level(db.Model):
|
157
|
+
__tablename__ = 'levels'
|
159
158
|
id = db.Column(db.Integer, primary_key=True)
|
160
159
|
org_id = db.Column(db.Integer, db.ForeignKey('organizations.id'), nullable=False)
|
161
160
|
skill_value = db.Column(db.Float) # A number from 0 (NHL) to 100 (pedestrian)
|
162
161
|
level_name = db.Column(db.String(100), unique=True)
|
163
162
|
level_alternative_name = db.Column(db.String(100))
|
164
163
|
is_seed = db.Column(db.Boolean, nullable=True, default=False) # New field
|
164
|
+
skill_propagation_sequence = db.Column(db.Integer, nullable=True, default=-1)
|
165
165
|
__table_args__ = (
|
166
166
|
db.UniqueConstraint('org_id', 'level_name', name='_org_level_name_uc'),
|
167
167
|
)
|
@@ -5,7 +5,7 @@ from collections import defaultdict
|
|
5
5
|
# Add the project root directory to the Python path
|
6
6
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
7
7
|
|
8
|
-
from hockey_blast_common_lib.models import Game, Division,
|
8
|
+
from hockey_blast_common_lib.models import Game, Division, Level, Season, League
|
9
9
|
from hockey_blast_common_lib.db_connection import create_session
|
10
10
|
|
11
11
|
def analyze_levels(org):
|
@@ -47,65 +47,27 @@ def analyze_levels(org):
|
|
47
47
|
|
48
48
|
session.close()
|
49
49
|
|
50
|
-
def reset_skill_values_in_divisions():
|
51
|
-
session = create_session("boss")
|
52
|
-
|
53
|
-
# Fetch all records from the Division table
|
54
|
-
divisions = session.query(Division).all()
|
55
|
-
|
56
|
-
for division in divisions:
|
57
|
-
# Look up the Skill table using the level from Division
|
58
|
-
div_level = division.level
|
59
|
-
|
60
|
-
# Query to find the matching Skill
|
61
|
-
skill = session.query(Skill).filter(Skill.org_id == division.org_id, Skill.level_name == div_level).one_or_none()
|
62
|
-
|
63
|
-
if not skill:
|
64
|
-
# If no match found, check each alternative name individually
|
65
|
-
skills = session.query(Skill).filter(Skill.org_id == division.org_id).all()
|
66
|
-
for s in skills:
|
67
|
-
alternative_names = s.level_alternative_name.split(',')
|
68
|
-
if div_level in alternative_names:
|
69
|
-
skill = s
|
70
|
-
break
|
71
|
-
|
72
|
-
if skill:
|
73
|
-
# Assign the skill_value and set skill_propagation_sequence to 0
|
74
|
-
division.skill_value = skill.skill_value
|
75
|
-
division.skill_propagation_sequence = 0
|
76
|
-
print(f"Assigned Skill {skill.skill_value} for Division with name {division.level}")
|
77
|
-
else:
|
78
|
-
# Assign -1 to skill_value and skill_propagation_sequence
|
79
|
-
division.skill_value = -1
|
80
|
-
division.skill_propagation_sequence = -1
|
81
|
-
print(f"No matching Skill found for Division with name {division.level}, assigned default values")
|
82
|
-
|
83
|
-
# Commit the changes to the Division
|
84
|
-
session.commit()
|
85
|
-
|
86
|
-
print("Skill values and propagation sequences have been populated into the Division table.")
|
87
|
-
|
88
50
|
def fill_seed_skills():
|
89
51
|
session = create_session("boss")
|
90
52
|
|
91
53
|
# List of Skill objects based on the provided comments
|
92
54
|
skills = [
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
55
|
+
Level(is_seed=True, org_id=1, skill_value=10.0, level_name='Adult Division 1', level_alternative_name='Senior A'),
|
56
|
+
Level(is_seed=True, org_id=1, skill_value=20.0, level_name='Adult Division 2', level_alternative_name='Senior B'),
|
57
|
+
Level(is_seed=True, org_id=1, skill_value=30.0, level_name='Adult Division 3A', level_alternative_name='Senior BB'),
|
58
|
+
Level(is_seed=True, org_id=1, skill_value=35.0, level_name='Adult Division 3B', level_alternative_name='Senior C'),
|
59
|
+
Level(is_seed=True, org_id=1, skill_value=40.0, level_name='Adult Division 4A', level_alternative_name='Senior CC'),
|
60
|
+
Level(is_seed=True, org_id=1, skill_value=45.0, level_name='Adult Division 4B', level_alternative_name='Senior CCC,Senior CCCC'),
|
61
|
+
Level(is_seed=True, org_id=1, skill_value=50.0, level_name='Adult Division 5A', level_alternative_name='Senior D,Senior DD'),
|
62
|
+
Level(is_seed=True, org_id=1, skill_value=55.0, level_name='Adult Division 5B', level_alternative_name='Senior DDD'),
|
63
|
+
Level(is_seed=True, org_id=1, skill_value=60.0, level_name='Adult Division 6A', level_alternative_name='Senior DDDD'),
|
64
|
+
Level(is_seed=True, org_id=1, skill_value=65.0, level_name='Adult Division 6B', level_alternative_name='Senior DDDDD'),
|
65
|
+
Level(is_seed=True, org_id=1, skill_value=70.0, level_name='Adult Division 7A', level_alternative_name='Senior E'),
|
66
|
+
Level(is_seed=True, org_id=1, skill_value=75.0, level_name='Adult Division 7B', level_alternative_name='Senior EE'),
|
67
|
+
Level(is_seed=True, org_id=1, skill_value=80.0, level_name='Adult Division 8', level_alternative_name='Senior EEE'),
|
68
|
+
Level(is_seed=True, org_id=1, skill_value=80.0, level_name='Adult Division 8A', level_alternative_name='Senior EEE'),
|
69
|
+
Level(is_seed=True, org_id=1, skill_value=85.0, level_name='Adult Division 8B', level_alternative_name='Senior EEEE'),
|
70
|
+
Level(is_seed=True, org_id=1, skill_value=90.0, level_name='Adult Division 9', level_alternative_name='Senior EEEEE')
|
109
71
|
]
|
110
72
|
|
111
73
|
for skill in skills:
|
@@ -116,9 +78,9 @@ def fill_seed_skills():
|
|
116
78
|
|
117
79
|
def get_fake_skill(session):
|
118
80
|
# Create a special fake Skill with org_id == -1 and skill_value == -1
|
119
|
-
fake_skill = session.query(
|
81
|
+
fake_skill = session.query(Level).filter_by(org_id=1, level_name='Fake Skill').first()
|
120
82
|
if not fake_skill:
|
121
|
-
fake_skill =
|
83
|
+
fake_skill = Level(
|
122
84
|
org_id=1,
|
123
85
|
skill_value=-1,
|
124
86
|
level_name='Fake Skill',
|
@@ -143,7 +105,7 @@ def delete_all_skills():
|
|
143
105
|
fake_skill = get_fake_skill(session)
|
144
106
|
assign_fake_skill_to_divisions(session, fake_skill)
|
145
107
|
# Delete all Skill records except the fake skill
|
146
|
-
session.query(
|
108
|
+
session.query(Level).filter(Level.id != fake_skill.id).delete(synchronize_session=False)
|
147
109
|
session.commit()
|
148
110
|
print("All Skill records except the fake skill have been deleted.")
|
149
111
|
|
@@ -177,7 +139,7 @@ def populate_league_ids():
|
|
177
139
|
|
178
140
|
if __name__ == "__main__":
|
179
141
|
# delete_all_skills()
|
180
|
-
#
|
142
|
+
#fill_seed_skills()
|
181
143
|
reset_skill_values_in_divisions()
|
182
144
|
#populate_season_ids() # Call the function to populate season_ids
|
183
145
|
#populate_league_ids() # Call the new function to populate league_ids
|
@@ -0,0 +1,368 @@
|
|
1
|
+
import sys
|
2
|
+
import os
|
3
|
+
from collections import defaultdict
|
4
|
+
import numpy as np
|
5
|
+
|
6
|
+
# Add the project root directory to the Python path
|
7
|
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
8
|
+
|
9
|
+
from hockey_blast_common_lib.models import Level, Division
|
10
|
+
from hockey_blast_common_lib.stats_models import LevelsGraphEdge, LevelStatsSkater, SkillValuePPGRatio
|
11
|
+
from hockey_blast_common_lib.db_connection import create_session
|
12
|
+
from sqlalchemy import func
|
13
|
+
|
14
|
+
import numpy as np
|
15
|
+
|
16
|
+
class Config:
|
17
|
+
MIN_GAMES_PLAYED_FOR_EDGE = 10
|
18
|
+
MIN_PPG_FOR_EDGE = 0.5
|
19
|
+
MIN_HUMANS_FOR_EDGE = 2
|
20
|
+
MAX_PROPAGATION_SEQUENCE = 4
|
21
|
+
MIN_CONNECTIONS_FOR_CORRELATION = 20
|
22
|
+
MIN_CONNECTIONS_FOR_PROPAGATION = 5
|
23
|
+
MAX_SKILL_DIFF_IN_EDGE = 30
|
24
|
+
|
25
|
+
@staticmethod
|
26
|
+
def discard_outliers(data, m=2):
|
27
|
+
"""
|
28
|
+
Discard outliers from the data using the modified Z-score method.
|
29
|
+
:param data: List of data points
|
30
|
+
:param m: Threshold for the modified Z-score
|
31
|
+
:return: List of data points with outliers removed
|
32
|
+
"""
|
33
|
+
if len(data) == 0:
|
34
|
+
return data
|
35
|
+
median = np.median(data)
|
36
|
+
diff = np.abs(data - median)
|
37
|
+
med_abs_deviation = np.median(diff)
|
38
|
+
if med_abs_deviation == 0:
|
39
|
+
return data
|
40
|
+
modified_z_score = 0.6745 * diff / med_abs_deviation
|
41
|
+
return data[modified_z_score < m]
|
42
|
+
|
43
|
+
def reset_skill_values_in_divisions():
|
44
|
+
session = create_session("boss")
|
45
|
+
|
46
|
+
# Fetch all records from the Division table
|
47
|
+
divisions = session.query(Division).all()
|
48
|
+
|
49
|
+
for division in divisions:
|
50
|
+
# Look up the Skill table using the level from Division
|
51
|
+
div_level = division.level
|
52
|
+
# Query to find the matching Skill
|
53
|
+
level = session.query(Level).filter(Level.org_id == division.org_id, Level.level_name == div_level).one_or_none()
|
54
|
+
|
55
|
+
if not level:
|
56
|
+
# If no match found, check each alternative name individually
|
57
|
+
skills = session.query(Level).filter(Level.org_id == division.org_id).all()
|
58
|
+
for s in skills:
|
59
|
+
alternative_names = s.level_alternative_name.split(',')
|
60
|
+
if div_level in alternative_names:
|
61
|
+
level = s
|
62
|
+
break
|
63
|
+
|
64
|
+
if level:
|
65
|
+
# Assign the skill_value and set skill_propagation_sequence to 0
|
66
|
+
division.level_id = level.id
|
67
|
+
if level.is_seed:
|
68
|
+
level.skill_propagation_sequence = 0
|
69
|
+
else:
|
70
|
+
level.skill_propagation_sequence = -1
|
71
|
+
level.skill_value = -1
|
72
|
+
else:
|
73
|
+
# Add new Skill with values previously used for division
|
74
|
+
new_level = Level(
|
75
|
+
org_id=division.org_id,
|
76
|
+
skill_value=-1,
|
77
|
+
level_name=division.level,
|
78
|
+
level_alternative_name='',
|
79
|
+
is_seed=False,
|
80
|
+
skill_propagation_sequence=-1
|
81
|
+
)
|
82
|
+
session.add(new_level)
|
83
|
+
session.commit()
|
84
|
+
division.skill_id = new_level.id
|
85
|
+
print(f"Created new Level for Division {division.level}")
|
86
|
+
|
87
|
+
# Commit the changes to the Division
|
88
|
+
session.commit()
|
89
|
+
|
90
|
+
print("Level values and propagation sequences have been populated into the Division table.")
|
91
|
+
|
92
|
+
def build_levels_graph_edges():
|
93
|
+
# Creates unique edges from levelA to levelB (there is no reverse edge levelB to levelA)
|
94
|
+
session = create_session("boss")
|
95
|
+
|
96
|
+
# Delete all existing edges
|
97
|
+
session.query(LevelsGraphEdge).delete()
|
98
|
+
session.commit()
|
99
|
+
|
100
|
+
# Query to get all level stats
|
101
|
+
level_stats = session.query(LevelStatsSkater).all()
|
102
|
+
|
103
|
+
# Dictionary to store stats by level and human
|
104
|
+
level_human_stats = defaultdict(lambda: defaultdict(dict))
|
105
|
+
|
106
|
+
for stat in level_stats:
|
107
|
+
if stat.games_played >= Config.MIN_GAMES_PLAYED_FOR_EDGE and stat.points_per_game >= Config.MIN_PPG_FOR_EDGE:
|
108
|
+
level_human_stats[stat.level_id][stat.human_id] = {
|
109
|
+
'games_played': stat.games_played,
|
110
|
+
'points_per_game': stat.points_per_game
|
111
|
+
}
|
112
|
+
|
113
|
+
# Dictionary to store edges
|
114
|
+
edges = {}
|
115
|
+
|
116
|
+
# Build edges
|
117
|
+
total_levels = len(level_human_stats)
|
118
|
+
processed_levels = 0
|
119
|
+
for from_level_id, from_humans in level_human_stats.items():
|
120
|
+
from_level = session.query(Level).filter_by(id=from_level_id).first()
|
121
|
+
for to_level_id, to_humans in level_human_stats.items():
|
122
|
+
to_level = session.query(Level).filter_by(id=to_level_id).first()
|
123
|
+
if from_level.id >= to_level.id:
|
124
|
+
continue
|
125
|
+
|
126
|
+
common_humans = set(from_humans.keys()) & set(to_humans.keys())
|
127
|
+
n_connections = len(common_humans)
|
128
|
+
n_games = 0
|
129
|
+
|
130
|
+
if n_connections < Config.MIN_HUMANS_FOR_EDGE:
|
131
|
+
continue
|
132
|
+
|
133
|
+
ppg_ratios = []
|
134
|
+
# if from_level.id == 223 and to_level.id == 219: #216
|
135
|
+
# print(f"Debug: From Level ID: {from_level.id}, To Level ID: {to_level.id}")
|
136
|
+
for human_id in common_humans:
|
137
|
+
from_ppg = from_humans[human_id]['points_per_game']
|
138
|
+
to_ppg = to_humans[human_id]['points_per_game']
|
139
|
+
from_games = from_humans[human_id]['games_played']
|
140
|
+
to_games = to_humans[human_id]['games_played']
|
141
|
+
min_games = min(from_games, to_games)
|
142
|
+
n_games += min_games
|
143
|
+
|
144
|
+
# if from_level.id == 223 and to_level.id == 219: #216
|
145
|
+
# print(f"Human {human_id} From PPG: {from_ppg}, To PPG: {to_ppg}, Min Games: {min_games} n_games: {n_games}")
|
146
|
+
|
147
|
+
if from_ppg > 0 and to_ppg > 0:
|
148
|
+
ppg_ratios.append(to_ppg / from_ppg)
|
149
|
+
|
150
|
+
if not ppg_ratios:
|
151
|
+
continue
|
152
|
+
|
153
|
+
# Discard outliers
|
154
|
+
ppg_ratios = Config.discard_outliers(np.array(ppg_ratios))
|
155
|
+
|
156
|
+
if len(ppg_ratios) == 0:
|
157
|
+
continue
|
158
|
+
|
159
|
+
avg_ppg_ratio = float(sum(ppg_ratios) / len(ppg_ratios))
|
160
|
+
|
161
|
+
# if sorted([from_level.id, to_level.id]) == [219, 223]:
|
162
|
+
# print(f"From {from_level_id} to {to_level_id} n_connections {n_connections} n_games: {n_games}")
|
163
|
+
|
164
|
+
edge = LevelsGraphEdge(
|
165
|
+
from_level_id=from_level_id,
|
166
|
+
to_level_id=to_level_id,
|
167
|
+
n_connections=n_connections,
|
168
|
+
ppg_ratio=avg_ppg_ratio,
|
169
|
+
n_games=n_games # Store the number of games
|
170
|
+
)
|
171
|
+
edges[(from_level_id, to_level_id)] = edge
|
172
|
+
|
173
|
+
processed_levels += 1
|
174
|
+
print(f"\rProcessed {processed_levels}/{total_levels} levels ({(processed_levels/total_levels)*100:.2f}%)", end="")
|
175
|
+
|
176
|
+
# Insert edges into the database
|
177
|
+
for edge in edges.values():
|
178
|
+
session.add(edge)
|
179
|
+
session.commit()
|
180
|
+
|
181
|
+
print("\nLevels graph edges have been populated into the database.")
|
182
|
+
|
183
|
+
def propagate_skill_levels(propagation_sequence):
|
184
|
+
min_skill_value = float('inf')
|
185
|
+
max_skill_value = float('-inf')
|
186
|
+
|
187
|
+
session = create_session("boss")
|
188
|
+
|
189
|
+
if propagation_sequence == 0:
|
190
|
+
# Delete all existing correlation data
|
191
|
+
session.query(SkillValuePPGRatio).delete()
|
192
|
+
session.commit()
|
193
|
+
|
194
|
+
# Build and save the correlation data
|
195
|
+
levels = session.query(Level).filter(Level.skill_propagation_sequence == 0).all()
|
196
|
+
level_ids = {level.id for level in levels}
|
197
|
+
correlation_data = defaultdict(list)
|
198
|
+
|
199
|
+
for level in levels:
|
200
|
+
if level.skill_value == -1:
|
201
|
+
continue
|
202
|
+
|
203
|
+
edges = session.query(LevelsGraphEdge).filter(
|
204
|
+
(LevelsGraphEdge.from_level_id == level.id) |
|
205
|
+
(LevelsGraphEdge.to_level_id == level.id)
|
206
|
+
).all()
|
207
|
+
|
208
|
+
for edge in edges:
|
209
|
+
if edge.n_connections < Config.MIN_CONNECTIONS_FOR_CORRELATION:
|
210
|
+
continue
|
211
|
+
|
212
|
+
if edge.from_level_id == level.id:
|
213
|
+
target_level_id = edge.to_level_id
|
214
|
+
ppg_ratio_edge = edge.ppg_ratio
|
215
|
+
else:
|
216
|
+
# We go over same edge twice in this logic, let's skip the reverse edge
|
217
|
+
continue
|
218
|
+
|
219
|
+
if target_level_id not in level_ids:
|
220
|
+
continue
|
221
|
+
|
222
|
+
target_level = session.query(Level).filter_by(id=target_level_id).first()
|
223
|
+
if target_level:
|
224
|
+
skill_value_from = level.skill_value
|
225
|
+
skill_value_to = target_level.skill_value
|
226
|
+
|
227
|
+
# Same skill value - no correlation
|
228
|
+
if skill_value_from == skill_value_to:
|
229
|
+
continue
|
230
|
+
|
231
|
+
|
232
|
+
# Since we go over all levels in the sequence 0, we will see each edge twice
|
233
|
+
# This condition eliminates duplicates
|
234
|
+
if abs(skill_value_from - skill_value_to) > Config.MAX_SKILL_DIFF_IN_EDGE:
|
235
|
+
continue
|
236
|
+
|
237
|
+
# Debug prints
|
238
|
+
# print(f"From Skill {level.skill_value} to {target_level.skill_value} ratio: {ppg_ratio}")
|
239
|
+
|
240
|
+
# Ensure INCREASING SKILL VALUES for the correlation data!
|
241
|
+
if skill_value_from > skill_value_to:
|
242
|
+
skill_value_from, skill_value_to = skill_value_to, skill_value_from
|
243
|
+
ppg_ratio_edge = 1 / ppg_ratio_edge
|
244
|
+
|
245
|
+
correlation_data[(skill_value_from, skill_value_to)].append(
|
246
|
+
(ppg_ratio_edge, edge.n_games)
|
247
|
+
)
|
248
|
+
|
249
|
+
# Save correlation data to the database
|
250
|
+
for (skill_value_from, skill_value_to), ppg_ratios in correlation_data.items():
|
251
|
+
ppg_ratios = [(ppg_ratio, n_games) for ppg_ratio, n_games in ppg_ratios]
|
252
|
+
ppg_ratios_array = np.array(ppg_ratios, dtype=[('ppg_ratio', float), ('n_games', int)])
|
253
|
+
ppg_ratios_filtered = Config.discard_outliers(ppg_ratios_array['ppg_ratio'])
|
254
|
+
if len(ppg_ratios_filtered) > 0:
|
255
|
+
avg_ppg_ratio = float(sum(ppg_ratio * n_games for ppg_ratio, n_games in ppg_ratios if ppg_ratio in ppg_ratios_filtered) / sum(n_games for ppg_ratio, n_games in ppg_ratios if ppg_ratio in ppg_ratios_filtered))
|
256
|
+
total_n_games = sum(n_games for ppg_ratio, n_games in ppg_ratios if ppg_ratio in ppg_ratios_filtered)
|
257
|
+
correlation = SkillValuePPGRatio(
|
258
|
+
from_skill_value=skill_value_from,
|
259
|
+
to_skill_value=skill_value_to,
|
260
|
+
ppg_ratio=avg_ppg_ratio,
|
261
|
+
n_games=total_n_games # Store the sum of games
|
262
|
+
)
|
263
|
+
session.add(correlation)
|
264
|
+
session.commit()
|
265
|
+
# Update min and max skill values
|
266
|
+
min_skill_value = min(min_skill_value, skill_value_from, skill_value_to)
|
267
|
+
max_skill_value = max(max_skill_value, skill_value_from, skill_value_to)
|
268
|
+
|
269
|
+
# Propagate skill levels
|
270
|
+
levels = session.query(Level).filter(Level.skill_propagation_sequence == propagation_sequence).all()
|
271
|
+
suggested_skill_values = defaultdict(list)
|
272
|
+
|
273
|
+
for level in levels:
|
274
|
+
edges = session.query(LevelsGraphEdge).filter(
|
275
|
+
(LevelsGraphEdge.from_level_id == level.id) |
|
276
|
+
(LevelsGraphEdge.to_level_id == level.id)
|
277
|
+
).all()
|
278
|
+
|
279
|
+
for edge in edges:
|
280
|
+
if edge.n_connections < Config.MIN_CONNECTIONS_FOR_PROPAGATION:
|
281
|
+
continue
|
282
|
+
|
283
|
+
if edge.from_level_id == level.id:
|
284
|
+
target_level_id = edge.to_level_id
|
285
|
+
ppg_ratio_edge = edge.ppg_ratio
|
286
|
+
else:
|
287
|
+
target_level_id = edge.from_level_id
|
288
|
+
ppg_ratio_edge = 1 / edge.ppg_ratio
|
289
|
+
|
290
|
+
target_level = session.query(Level).filter_by(id=target_level_id).first()
|
291
|
+
if target_level and target_level.skill_propagation_sequence == -1:
|
292
|
+
correlations = session.query(SkillValuePPGRatio).filter(
|
293
|
+
(SkillValuePPGRatio.from_skill_value <= level.skill_value) &
|
294
|
+
(SkillValuePPGRatio.to_skill_value >= level.skill_value)
|
295
|
+
).all()
|
296
|
+
|
297
|
+
if correlations:
|
298
|
+
weighted_skill_values = []
|
299
|
+
for correlation in correlations:
|
300
|
+
# Skill value always increases in the correlation data
|
301
|
+
# Let's avoid extrapolating from the end of the edge and away from the edge!
|
302
|
+
|
303
|
+
# Check left side of the edge
|
304
|
+
if (level.skill_value == correlation.from_skill_value and level.skill_value > min_skill_value):
|
305
|
+
if ppg_ratio_edge < 1:
|
306
|
+
continue
|
307
|
+
# Check right side of the edge
|
308
|
+
if (level.skill_value == correlation.to_skill_value and level.skill_value < max_skill_value):
|
309
|
+
if ppg_ratio_edge > 1:
|
310
|
+
continue
|
311
|
+
|
312
|
+
|
313
|
+
# First confirm which way are we going here
|
314
|
+
if (ppg_ratio_edge < 1 and correlation.ppg_ratio > 1) or (ppg_ratio_edge > 1 and correlation.ppg_ratio < 1):
|
315
|
+
# Reverse the correlation
|
316
|
+
from_skill_value=correlation.to_skill_value
|
317
|
+
to_skill_value=correlation.from_skill_value
|
318
|
+
ppg_ratio_range = 1 / correlation.ppg_ratio
|
319
|
+
else:
|
320
|
+
from_skill_value=correlation.from_skill_value
|
321
|
+
to_skill_value=correlation.to_skill_value
|
322
|
+
ppg_ratio_range = correlation.ppg_ratio
|
323
|
+
|
324
|
+
# Now both ratios are either < 1 or > 1
|
325
|
+
if ppg_ratio_edge < 1:
|
326
|
+
ppg_ratio_for_extrapolation = 1 / ppg_ratio_edge
|
327
|
+
ppg_ratio_range = 1 / ppg_ratio_range
|
328
|
+
else:
|
329
|
+
ppg_ratio_for_extrapolation = ppg_ratio_edge
|
330
|
+
|
331
|
+
# Interpolate or extrapolate skill value
|
332
|
+
skill_value_range = to_skill_value - from_skill_value
|
333
|
+
skill_value_diff = (ppg_ratio_for_extrapolation / ppg_ratio_range) * skill_value_range
|
334
|
+
new_skill_value = level.skill_value + skill_value_diff
|
335
|
+
weighted_skill_values.append((new_skill_value, correlation.n_games))
|
336
|
+
# if target_level.id == 229:
|
337
|
+
# print(f"Debug: From Level ID: {level.id}, To Level ID: {target_level.id}")
|
338
|
+
# print(f"Debug: From Skill Value: {level.skill_value} PPG Ratio: {ppg_ratio_for_extrapolation}, PPG Ratio Range: {ppg_ratio_range}")
|
339
|
+
# print(f"Debug: Skill Value Range: {skill_value_range}, Skill Value Diff: {skill_value_diff}")
|
340
|
+
# print(f"Debug: New Skill Value: {new_skill_value}")
|
341
|
+
|
342
|
+
# Calculate weighted average of new skill values
|
343
|
+
total_n_games = sum(n_games for _, n_games in weighted_skill_values)
|
344
|
+
weighted_avg_skill_value = sum(skill_value * n_games for skill_value, n_games in weighted_skill_values) / total_n_games
|
345
|
+
suggested_skill_values[target_level_id].append(weighted_avg_skill_value)
|
346
|
+
|
347
|
+
# Update skill values for target levels
|
348
|
+
for target_level_id, skill_values in suggested_skill_values.items():
|
349
|
+
skill_values = Config.discard_outliers(np.array(skill_values))
|
350
|
+
if len(skill_values) > 0:
|
351
|
+
avg_skill_value = float(sum(skill_values) / len(skill_values))
|
352
|
+
avg_skill_value = max(avg_skill_value, 9.6)
|
353
|
+
if avg_skill_value < min_skill_value:
|
354
|
+
avg_skill_value = min_skill_value - 0.01
|
355
|
+
session.query(Level).filter_by(id=target_level_id).update({
|
356
|
+
'skill_value': avg_skill_value,
|
357
|
+
'skill_propagation_sequence': propagation_sequence + 1
|
358
|
+
})
|
359
|
+
session.commit()
|
360
|
+
|
361
|
+
print(f"Skill levels have been propagated for sequence {propagation_sequence}.")
|
362
|
+
|
363
|
+
if __name__ == "__main__":
|
364
|
+
reset_skill_values_in_divisions()
|
365
|
+
build_levels_graph_edges()
|
366
|
+
|
367
|
+
for sequence in range(Config.MAX_PROPAGATION_SEQUENCE + 1):
|
368
|
+
propagate_skill_levels(sequence)
|
@@ -222,6 +222,20 @@ class DivisionStatsSkater(BaseStatsSkater):
|
|
222
222
|
def get_aggregation_column(cls):
|
223
223
|
return 'division_id'
|
224
224
|
|
225
|
+
class LevelStatsSkater(BaseStatsSkater):
|
226
|
+
__tablename__ = 'level_stats_skater'
|
227
|
+
level_id = db.Column(db.Integer, db.ForeignKey('levels.id'), nullable=False)
|
228
|
+
aggregation_id = synonym('level_id')
|
229
|
+
|
230
|
+
@declared_attr
|
231
|
+
def aggregation_type(cls):
|
232
|
+
return 'level'
|
233
|
+
|
234
|
+
@classmethod
|
235
|
+
def get_aggregation_column(cls):
|
236
|
+
return 'level_id'
|
237
|
+
|
238
|
+
|
225
239
|
class OrgStatsGoalie(BaseStatsGoalie):
|
226
240
|
__tablename__ = 'org_stats_goalie'
|
227
241
|
org_id = db.Column(db.Integer, db.ForeignKey('organizations.id'), nullable=False)
|
@@ -561,3 +575,40 @@ class DivisionStatsWeeklyScorekeeper(BaseStatsScorekeeper):
|
|
561
575
|
@classmethod
|
562
576
|
def get_aggregation_column(cls):
|
563
577
|
return 'division_id'
|
578
|
+
|
579
|
+
class LevelsGraphEdge(db.Model):
|
580
|
+
__tablename__ = 'levels_graph_edges'
|
581
|
+
id = db.Column(db.Integer, primary_key=True)
|
582
|
+
from_level_id = db.Column(db.Integer, db.ForeignKey('levels.id'), nullable=False)
|
583
|
+
to_level_id = db.Column(db.Integer, db.ForeignKey('levels.id'), nullable=False)
|
584
|
+
n_connections = db.Column(db.Integer, nullable=False)
|
585
|
+
ppg_ratio = db.Column(db.Float, nullable=False)
|
586
|
+
n_games = db.Column(db.Integer, nullable=False) # New field to store the number of games
|
587
|
+
|
588
|
+
__table_args__ = (
|
589
|
+
db.UniqueConstraint('from_level_id', 'to_level_id', name='_from_to_level_uc'),
|
590
|
+
)
|
591
|
+
|
592
|
+
class SkillPropagationCorrelation(db.Model):
|
593
|
+
__tablename__ = 'skill_propagation_correlation'
|
594
|
+
id = db.Column(db.Integer, primary_key=True)
|
595
|
+
skill_value_from = db.Column(db.Float, nullable=False)
|
596
|
+
skill_value_to = db.Column(db.Float, nullable=False)
|
597
|
+
ppg_ratio = db.Column(db.Float, nullable=False)
|
598
|
+
|
599
|
+
__table_args__ = (
|
600
|
+
db.UniqueConstraint('skill_value_from', 'skill_value_to', 'ppg_ratio', name='_skill_value_ppg_ratio_uc'),
|
601
|
+
)
|
602
|
+
|
603
|
+
# How PPG changes with INCREASING SKILL VALUES
|
604
|
+
class SkillValuePPGRatio(db.Model):
|
605
|
+
__tablename__ = 'skill_value_ppg_ratios'
|
606
|
+
id = db.Column(db.Integer, primary_key=True)
|
607
|
+
from_skill_value = db.Column(db.Float, nullable=False)
|
608
|
+
to_skill_value = db.Column(db.Float, nullable=False)
|
609
|
+
ppg_ratio = db.Column(db.Float, nullable=False)
|
610
|
+
n_games = db.Column(db.Integer, nullable=False) # New field to store the sum of games
|
611
|
+
|
612
|
+
__table_args__ = (
|
613
|
+
db.UniqueConstraint('from_skill_value', 'to_skill_value', name='_from_to_skill_value_uc'),
|
614
|
+
)
|
@@ -0,0 +1,21 @@
|
|
1
|
+
hockey_blast_common_lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
hockey_blast_common_lib/aggregate_goalie_stats.py,sha256=d2qav46Rg2DNIYRj_Ubj1kpQmoPUJHKiwEWOVU25nD4,8742
|
3
|
+
hockey_blast_common_lib/aggregate_human_stats.py,sha256=88OMhTgQjzc9xIakf6kW9_lZwbSXkpsZy8C0pX-Wlq8,14229
|
4
|
+
hockey_blast_common_lib/aggregate_referee_stats.py,sha256=A0PTyEbPUjqfXxlJCDOVioFaQk9AyjjhiWEuRuu35v0,11036
|
5
|
+
hockey_blast_common_lib/aggregate_skater_stats.py,sha256=jkBD5u-gJc1DTDIEuxM_qymKsrWtLagFKeEn__2rFgU,16009
|
6
|
+
hockey_blast_common_lib/assign_skater_skill.py,sha256=YQYYWKArkvqPsu7shlRzggE-p_7rcEMO3VtnzKQHPG0,1657
|
7
|
+
hockey_blast_common_lib/db_connection.py,sha256=HvPxDvOj7j5H85RfslGvHVNevfg7mKCd0syJ6NX21mU,1890
|
8
|
+
hockey_blast_common_lib/dump_sample_db.sh,sha256=MHPA-Ciod7wsvAlMbRtXFiyajgnEqU1xR59sJQ9UWR0,738
|
9
|
+
hockey_blast_common_lib/hockey_blast_sample_backup.sql.gz,sha256=Flm7M1rDuz-psuBg_gtlo1JhjjJRjnBMGy-gTwgMhjM,1033685
|
10
|
+
hockey_blast_common_lib/models.py,sha256=2YkFHgzvxLSU2-fURsyIqdaXjcQwZ_wjauYR9wWQtDw,15924
|
11
|
+
hockey_blast_common_lib/options.py,sha256=6na8fo-5A2RBPpd_h-7dsqetOLSLoNEJg1QMYgl4jNs,792
|
12
|
+
hockey_blast_common_lib/restore_sample_db.sh,sha256=u2zKazC6vNMULkpYzI64nlneCWaGUtDHPBAU-gWgRbw,1861
|
13
|
+
hockey_blast_common_lib/skills_in_divisions.py,sha256=tQYSikIV9AN_Cr-770KRy4reaiiR-2vvtwaNPJ8Lvws,7524
|
14
|
+
hockey_blast_common_lib/skills_propagation.py,sha256=x6yy7fJ6IX3YiHqiP_v7-p_S2Expb8JJ-mWuajEFBdY,16388
|
15
|
+
hockey_blast_common_lib/stats_models.py,sha256=35-6iz1r8MJcmzlyIlJy0uHgWh8oltyf-3H61ocal3o,23048
|
16
|
+
hockey_blast_common_lib/utils.py,sha256=odDJWCK0BgbResXeoUzxbVChjaxcXr168ZxbrAw3L_8,3752
|
17
|
+
hockey_blast_common_lib/wsgi.py,sha256=7LGUzioigviJp-EUhSEaQcd4jBae0mxbkyBscQfZhlc,730
|
18
|
+
hockey_blast_common_lib-0.1.31.dist-info/METADATA,sha256=F19EMmlAfN34YH4YhXsATH-TdZ3ROGICcIU-T10A5yw,318
|
19
|
+
hockey_blast_common_lib-0.1.31.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
20
|
+
hockey_blast_common_lib-0.1.31.dist-info/top_level.txt,sha256=wIR4LIkE40npoA2QlOdfCYlgFeGbsHR8Z6r0h46Vtgc,24
|
21
|
+
hockey_blast_common_lib-0.1.31.dist-info/RECORD,,
|
@@ -1,19 +0,0 @@
|
|
1
|
-
hockey_blast_common_lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
hockey_blast_common_lib/aggregate_goalie_stats.py,sha256=d2qav46Rg2DNIYRj_Ubj1kpQmoPUJHKiwEWOVU25nD4,8742
|
3
|
-
hockey_blast_common_lib/aggregate_human_stats.py,sha256=88OMhTgQjzc9xIakf6kW9_lZwbSXkpsZy8C0pX-Wlq8,14229
|
4
|
-
hockey_blast_common_lib/aggregate_referee_stats.py,sha256=A0PTyEbPUjqfXxlJCDOVioFaQk9AyjjhiWEuRuu35v0,11036
|
5
|
-
hockey_blast_common_lib/aggregate_skater_stats.py,sha256=37fhgej9trukr8cGaK7DT1HoxBcp95qwsypYCCziqqc,15563
|
6
|
-
hockey_blast_common_lib/db_connection.py,sha256=HvPxDvOj7j5H85RfslGvHVNevfg7mKCd0syJ6NX21mU,1890
|
7
|
-
hockey_blast_common_lib/dump_sample_db.sh,sha256=MHPA-Ciod7wsvAlMbRtXFiyajgnEqU1xR59sJQ9UWR0,738
|
8
|
-
hockey_blast_common_lib/hockey_blast_sample_backup.sql.gz,sha256=DcRvR7oyP2gbVU7fYUP8-Z9R79kl0jWt4zgFyNECstk,1033685
|
9
|
-
hockey_blast_common_lib/models.py,sha256=H8d58xZqD6bUiWrdhu8xeeDXYUuWyyKNyzWLVm5IerE,15852
|
10
|
-
hockey_blast_common_lib/options.py,sha256=-LtEX8duw5Pl3CSpjFlLM5FPvrZuTAxTfSlDPa7H6mQ,761
|
11
|
-
hockey_blast_common_lib/restore_sample_db.sh,sha256=u2zKazC6vNMULkpYzI64nlneCWaGUtDHPBAU-gWgRbw,1861
|
12
|
-
hockey_blast_common_lib/skills_in_divisions.py,sha256=lOVww6ff7LBANmcgFlv1Eh-fvPPq0E9noG8tSfUuIZc,9140
|
13
|
-
hockey_blast_common_lib/stats_models.py,sha256=PI-mL1jmjCHLAvaATxSsjHEn05g9L_reA_YpsITPWjQ,21047
|
14
|
-
hockey_blast_common_lib/utils.py,sha256=odDJWCK0BgbResXeoUzxbVChjaxcXr168ZxbrAw3L_8,3752
|
15
|
-
hockey_blast_common_lib/wsgi.py,sha256=7LGUzioigviJp-EUhSEaQcd4jBae0mxbkyBscQfZhlc,730
|
16
|
-
hockey_blast_common_lib-0.1.28.dist-info/METADATA,sha256=09IAnWyeWgm3Uy9FQSRU99XOSbbFXC9nV22Z3eHeAX8,318
|
17
|
-
hockey_blast_common_lib-0.1.28.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
18
|
-
hockey_blast_common_lib-0.1.28.dist-info/top_level.txt,sha256=wIR4LIkE40npoA2QlOdfCYlgFeGbsHR8Z6r0h46Vtgc,24
|
19
|
-
hockey_blast_common_lib-0.1.28.dist-info/RECORD,,
|
File without changes
|
{hockey_blast_common_lib-0.1.28.dist-info → hockey_blast_common_lib-0.1.31.dist-info}/top_level.txt
RENAMED
File without changes
|