hockey-blast-common-lib 0.1.50__py3-none-any.whl → 0.1.53__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_goalie_stats.py +44 -25
- hockey_blast_common_lib/aggregate_h2h_stats.py +231 -0
- hockey_blast_common_lib/aggregate_human_stats.py +45 -26
- hockey_blast_common_lib/aggregate_referee_stats.py +44 -25
- hockey_blast_common_lib/aggregate_s2s_stats.py +143 -0
- hockey_blast_common_lib/aggregate_skater_stats.py +149 -36
- hockey_blast_common_lib/assign_skater_skill.py +27 -17
- hockey_blast_common_lib/h2h_models.py +107 -0
- hockey_blast_common_lib/hockey_blast_sample_backup.sql.gz +0 -0
- hockey_blast_common_lib/models.py +1 -0
- hockey_blast_common_lib/progress_utils.py +91 -0
- hockey_blast_common_lib/skills_propagation.py +56 -30
- hockey_blast_common_lib/stats_models.py +6 -0
- hockey_blast_common_lib/wsgi.py +1 -0
- {hockey_blast_common_lib-0.1.50.dist-info → hockey_blast_common_lib-0.1.53.dist-info}/METADATA +1 -1
- hockey_blast_common_lib-0.1.53.dist-info/RECORD +27 -0
- hockey_blast_common_lib-0.1.50.dist-info/RECORD +0 -23
- {hockey_blast_common_lib-0.1.50.dist-info → hockey_blast_common_lib-0.1.53.dist-info}/WHEEL +0 -0
- {hockey_blast_common_lib-0.1.50.dist-info → hockey_blast_common_lib-0.1.53.dist-info}/top_level.txt +0 -0
@@ -16,6 +16,53 @@ from hockey_blast_common_lib.utils import get_start_datetime
|
|
16
16
|
from sqlalchemy import func, case, and_
|
17
17
|
from collections import defaultdict
|
18
18
|
from hockey_blast_common_lib.stats_utils import ALL_ORGS_ID
|
19
|
+
from hockey_blast_common_lib.progress_utils import create_progress_tracker
|
20
|
+
|
21
|
+
def calculate_current_point_streak(session, human_id, filter_condition):
|
22
|
+
"""
|
23
|
+
Calculate the current point streak for a player.
|
24
|
+
A point streak is consecutive games (from the most recent game backward) where the player had at least one point.
|
25
|
+
Returns a tuple: (streak_length, average_points_during_streak)
|
26
|
+
"""
|
27
|
+
# Get all games for this player ordered by date/time descending (most recent first)
|
28
|
+
games = session.query(Game).join(GameRoster, Game.id == GameRoster.game_id).filter(
|
29
|
+
GameRoster.human_id == human_id,
|
30
|
+
~GameRoster.role.ilike('g'), # Exclude goalie games
|
31
|
+
filter_condition,
|
32
|
+
Game.status.like('Final%') # Only final games
|
33
|
+
).order_by(Game.date.desc(), Game.time.desc()).all()
|
34
|
+
|
35
|
+
if not games:
|
36
|
+
return 0, 0.0
|
37
|
+
|
38
|
+
current_streak = 0
|
39
|
+
total_points_in_streak = 0
|
40
|
+
|
41
|
+
for game in games:
|
42
|
+
# Check if the player had any points in this game
|
43
|
+
goals = session.query(Goal).filter(
|
44
|
+
Goal.game_id == game.id,
|
45
|
+
Goal.goal_scorer_id == human_id
|
46
|
+
).count()
|
47
|
+
|
48
|
+
assists = session.query(Goal).filter(
|
49
|
+
Goal.game_id == game.id,
|
50
|
+
((Goal.assist_1_id == human_id) | (Goal.assist_2_id == human_id))
|
51
|
+
).count()
|
52
|
+
|
53
|
+
total_points = goals + assists
|
54
|
+
|
55
|
+
if total_points > 0:
|
56
|
+
current_streak += 1
|
57
|
+
total_points_in_streak += total_points
|
58
|
+
else:
|
59
|
+
# Streak is broken, stop counting
|
60
|
+
break
|
61
|
+
|
62
|
+
# Calculate average points during streak
|
63
|
+
avg_points_during_streak = total_points_in_streak / current_streak if current_streak > 0 else 0.0
|
64
|
+
|
65
|
+
return current_streak, avg_points_during_streak
|
19
66
|
|
20
67
|
def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_filter_out, debug_human_id=None, aggregation_window=None):
|
21
68
|
human_ids_to_filter = get_human_ids_by_names(session, names_to_filter_out)
|
@@ -57,7 +104,12 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
|
|
57
104
|
elif aggregation_type == 'level':
|
58
105
|
StatsModel = LevelStatsSkater
|
59
106
|
min_games = MIN_GAMES_FOR_LEVEL_STATS
|
60
|
-
|
107
|
+
# Get division IDs for this level to avoid cartesian product
|
108
|
+
division_ids = session.query(Division.id).filter(Division.level_id == aggregation_id).all()
|
109
|
+
division_ids = [div_id[0] for div_id in division_ids]
|
110
|
+
if not division_ids:
|
111
|
+
return # No divisions for this level
|
112
|
+
filter_condition = Game.division_id.in_(division_ids)
|
61
113
|
# Add filter to only include games for the last 5 years
|
62
114
|
# five_years_ago = datetime.now() - timedelta(days=5*365)
|
63
115
|
# level_window_filter = func.cast(func.concat(Game.date, ' ', Game.time), sqlalchemy.types.TIMESTAMP) >= five_years_ago
|
@@ -86,39 +138,65 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
|
|
86
138
|
# human_filter = [GameRoster.human_id == debug_human_id]
|
87
139
|
|
88
140
|
# Aggregate games played for each human in each division, excluding goalies
|
89
|
-
|
141
|
+
games_played_query = session.query(
|
90
142
|
GameRoster.human_id,
|
91
143
|
func.count(Game.id).label('games_played'),
|
92
144
|
func.array_agg(Game.id).label('game_ids')
|
93
|
-
).join(Game, Game.id == GameRoster.game_id)
|
145
|
+
).join(Game, Game.id == GameRoster.game_id)
|
146
|
+
|
147
|
+
# Only join Division if not level aggregation (since we filter on Game.division_id directly for levels)
|
148
|
+
if aggregation_type != 'level':
|
149
|
+
games_played_query = games_played_query.join(Division, Game.division_id == Division.id)
|
150
|
+
|
151
|
+
games_played_stats = games_played_query.filter(filter_condition, ~GameRoster.role.ilike('g'), *human_filter).group_by(GameRoster.human_id).all()
|
94
152
|
|
95
153
|
# Aggregate goals for each human in each division, excluding goalies
|
96
|
-
|
154
|
+
goals_query = session.query(
|
97
155
|
Goal.goal_scorer_id.label('human_id'),
|
98
156
|
func.count(Goal.id).label('goals'),
|
99
157
|
func.array_agg(Goal.game_id).label('goal_game_ids')
|
100
|
-
).join(Game, Game.id == Goal.game_id).join(GameRoster, and_(Game.id == GameRoster.game_id, Goal.goal_scorer_id == GameRoster.human_id))
|
158
|
+
).join(Game, Game.id == Goal.game_id).join(GameRoster, and_(Game.id == GameRoster.game_id, Goal.goal_scorer_id == GameRoster.human_id))
|
159
|
+
|
160
|
+
if aggregation_type != 'level':
|
161
|
+
goals_query = goals_query.join(Division, Game.division_id == Division.id)
|
162
|
+
|
163
|
+
goals_stats = goals_query.filter(filter_condition, ~GameRoster.role.ilike('g'), *human_filter).group_by(Goal.goal_scorer_id).all()
|
101
164
|
|
102
165
|
# Aggregate assists for each human in each division, excluding goalies
|
103
|
-
|
166
|
+
assists_query = session.query(
|
104
167
|
Goal.assist_1_id.label('human_id'),
|
105
168
|
func.count(Goal.id).label('assists'),
|
106
169
|
func.array_agg(Goal.game_id).label('assist_game_ids')
|
107
|
-
).join(Game, Game.id == Goal.game_id).join(GameRoster, and_(Game.id == GameRoster.game_id, Goal.assist_1_id == GameRoster.human_id))
|
108
|
-
|
109
|
-
|
170
|
+
).join(Game, Game.id == Goal.game_id).join(GameRoster, and_(Game.id == GameRoster.game_id, Goal.assist_1_id == GameRoster.human_id))
|
171
|
+
|
172
|
+
if aggregation_type != 'level':
|
173
|
+
assists_query = assists_query.join(Division, Game.division_id == Division.id)
|
174
|
+
|
175
|
+
assists_stats = assists_query.filter(filter_condition, ~GameRoster.role.ilike('g'), *human_filter).group_by(Goal.assist_1_id).all()
|
176
|
+
|
177
|
+
assists_query_2 = session.query(
|
110
178
|
Goal.assist_2_id.label('human_id'),
|
111
179
|
func.count(Goal.id).label('assists'),
|
112
180
|
func.array_agg(Goal.game_id).label('assist_2_game_ids')
|
113
|
-
).join(Game, Game.id == Goal.game_id).join(GameRoster, and_(Game.id == GameRoster.game_id, Goal.assist_2_id == GameRoster.human_id))
|
181
|
+
).join(Game, Game.id == Goal.game_id).join(GameRoster, and_(Game.id == GameRoster.game_id, Goal.assist_2_id == GameRoster.human_id))
|
182
|
+
|
183
|
+
if aggregation_type != 'level':
|
184
|
+
assists_query_2 = assists_query_2.join(Division, Game.division_id == Division.id)
|
185
|
+
|
186
|
+
assists_stats_2 = assists_query_2.filter(filter_condition, ~GameRoster.role.ilike('g'), *human_filter).group_by(Goal.assist_2_id).all()
|
114
187
|
|
115
188
|
# Aggregate penalties for each human in each division, excluding goalies
|
116
|
-
|
189
|
+
penalties_query = session.query(
|
117
190
|
Penalty.penalized_player_id.label('human_id'),
|
118
191
|
func.count(Penalty.id).label('penalties'),
|
119
192
|
func.sum(case((Penalty.penalty_minutes == 'GM', 1), else_=0)).label('gm_penalties'), # New aggregation for GM penalties
|
120
193
|
func.array_agg(Penalty.game_id).label('penalty_game_ids')
|
121
|
-
).join(Game, Game.id == Penalty.game_id).join(GameRoster, and_(Game.id == GameRoster.game_id, Penalty.penalized_player_id == GameRoster.human_id))
|
194
|
+
).join(Game, Game.id == Penalty.game_id).join(GameRoster, and_(Game.id == GameRoster.game_id, Penalty.penalized_player_id == GameRoster.human_id))
|
195
|
+
|
196
|
+
if aggregation_type != 'level':
|
197
|
+
penalties_query = penalties_query.join(Division, Game.division_id == Division.id)
|
198
|
+
|
199
|
+
penalties_stats = penalties_query.filter(filter_condition, ~GameRoster.role.ilike('g'), *human_filter).group_by(Penalty.penalized_player_id).all()
|
122
200
|
|
123
201
|
# Combine the results
|
124
202
|
stats_dict = {}
|
@@ -139,6 +217,8 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
|
|
139
217
|
'assists_per_game': 0.0,
|
140
218
|
'penalties_per_game': 0.0,
|
141
219
|
'gm_penalties_per_game': 0.0, # Initialize GM penalties per game
|
220
|
+
'current_point_streak': 0, # Initialize current point streak
|
221
|
+
'current_point_streak_avg_points': 0.0, # Initialize current point streak average points
|
142
222
|
'game_ids': [],
|
143
223
|
'first_game_id': None,
|
144
224
|
'last_game_id': None
|
@@ -194,6 +274,14 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
|
|
194
274
|
stat['first_game_id'] = first_game.id if first_game else None
|
195
275
|
stat['last_game_id'] = last_game.id if last_game else None
|
196
276
|
|
277
|
+
# Calculate current point streak (only for all-time stats)
|
278
|
+
if aggregation_window is None:
|
279
|
+
for key, stat in stats_dict.items():
|
280
|
+
aggregation_id, human_id = key
|
281
|
+
streak_length, avg_points = calculate_current_point_streak(session, human_id, filter_condition)
|
282
|
+
stat['current_point_streak'] = streak_length
|
283
|
+
stat['current_point_streak_avg_points'] = avg_points
|
284
|
+
|
197
285
|
# Calculate total_in_rank
|
198
286
|
total_in_rank = len(stats_dict)
|
199
287
|
|
@@ -214,6 +302,9 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
|
|
214
302
|
assign_ranks(stats_dict, 'assists_per_game')
|
215
303
|
assign_ranks(stats_dict, 'penalties_per_game')
|
216
304
|
assign_ranks(stats_dict, 'gm_penalties_per_game') # Assign ranks for GM penalties per game
|
305
|
+
if aggregation_window is None: # Only assign current_point_streak ranks for all-time stats
|
306
|
+
assign_ranks(stats_dict, 'current_point_streak')
|
307
|
+
assign_ranks(stats_dict, 'current_point_streak_avg_points')
|
217
308
|
|
218
309
|
# Debug output for specific human
|
219
310
|
if debug_human_id:
|
@@ -262,6 +353,10 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
|
|
262
353
|
penalties_per_game_rank=stat['penalties_per_game_rank'],
|
263
354
|
gm_penalties_per_game_rank=stat['gm_penalties_per_game_rank'], # Include GM penalties per game rank
|
264
355
|
total_in_rank=total_in_rank,
|
356
|
+
current_point_streak=stat.get('current_point_streak', 0),
|
357
|
+
current_point_streak_rank=stat.get('current_point_streak_rank', 0),
|
358
|
+
current_point_streak_avg_points=stat.get('current_point_streak_avg_points', 0.0),
|
359
|
+
current_point_streak_avg_points_rank=stat.get('current_point_streak_avg_points_rank', 0),
|
265
360
|
first_game_id=stat['first_game_id'],
|
266
361
|
last_game_id=stat['last_game_id']
|
267
362
|
)
|
@@ -281,33 +376,51 @@ def run_aggregate_skater_stats():
|
|
281
376
|
|
282
377
|
for org_id in org_ids:
|
283
378
|
division_ids = get_all_division_ids_for_org(session, org_id)
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
379
|
+
org_name = session.query(Organization.organization_name).filter(Organization.id == org_id).scalar() or f"org_id {org_id}"
|
380
|
+
|
381
|
+
if human_id_to_debug is None and division_ids:
|
382
|
+
# Process divisions with progress tracking
|
383
|
+
progress = create_progress_tracker(len(division_ids), f"Processing {len(division_ids)} divisions for {org_name}")
|
384
|
+
for i, division_id in enumerate(division_ids):
|
385
|
+
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)
|
386
|
+
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')
|
387
|
+
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')
|
388
|
+
progress.update(i + 1)
|
389
|
+
else:
|
390
|
+
# Debug mode or no divisions - process without progress tracking
|
391
|
+
for division_id in division_ids:
|
392
|
+
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)
|
393
|
+
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')
|
394
|
+
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')
|
395
|
+
|
396
|
+
# Process org-level stats with progress tracking
|
397
|
+
if human_id_to_debug is None:
|
398
|
+
org_progress = create_progress_tracker(3, f"Processing org-level stats for {org_name}")
|
399
|
+
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)
|
400
|
+
org_progress.update(1)
|
401
|
+
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')
|
402
|
+
org_progress.update(2)
|
403
|
+
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')
|
404
|
+
org_progress.update(3)
|
405
|
+
else:
|
406
|
+
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)
|
407
|
+
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')
|
408
|
+
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')
|
298
409
|
|
299
410
|
# Aggregate by level
|
300
411
|
level_ids = session.query(Division.level_id).distinct().all()
|
301
|
-
level_ids = [level_id[0] for level_id in level_ids]
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
412
|
+
level_ids = [level_id[0] for level_id in level_ids if level_id[0] is not None]
|
413
|
+
|
414
|
+
if human_id_to_debug is None and level_ids:
|
415
|
+
# Process levels with progress tracking
|
416
|
+
level_progress = create_progress_tracker(len(level_ids), f"Processing {len(level_ids)} skill levels")
|
417
|
+
for i, level_id in enumerate(level_ids):
|
418
|
+
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)
|
419
|
+
level_progress.update(i + 1)
|
420
|
+
else:
|
421
|
+
# Debug mode or no levels - process without progress tracking
|
422
|
+
for level_id in level_ids:
|
423
|
+
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)
|
311
424
|
|
312
425
|
if __name__ == "__main__":
|
313
426
|
run_aggregate_skater_stats()
|
@@ -6,43 +6,53 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
6
6
|
from hockey_blast_common_lib.models import Human, Level
|
7
7
|
from hockey_blast_common_lib.stats_models import LevelStatsSkater
|
8
8
|
from hockey_blast_common_lib.db_connection import create_session
|
9
|
-
from
|
9
|
+
from hockey_blast_common_lib.progress_utils import create_progress_tracker
|
10
10
|
|
11
|
-
def calculate_skater_skill_value(session,
|
12
|
-
|
11
|
+
def calculate_skater_skill_value(session, level_stats):
|
12
|
+
min_skill_value = float('inf') # Start with infinity since we want the minimum
|
13
13
|
|
14
14
|
for stat in level_stats:
|
15
15
|
level = session.query(Level).filter(Level.id == stat.level_id).first()
|
16
16
|
if not level or level.skill_value < 0:
|
17
17
|
continue
|
18
18
|
level_skill_value = level.skill_value
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
19
|
+
|
20
|
+
# Fix critical bug: Invert rank ratios so better players (lower ranks) get higher skill values
|
21
|
+
# Rank 1 (best) should get factor close to 1.0, worst rank should get factor close to 0.0
|
22
|
+
if stat.total_in_rank > 1:
|
23
|
+
ppg_skill_factor = 1 - (stat.points_per_game_rank - 1) / (stat.total_in_rank - 1)
|
24
|
+
else:
|
25
|
+
ppg_skill_factor = 1.0 # Only one player in level
|
26
|
+
|
27
|
+
# Apply skill adjustment: range from 0.8 to 1.2 of level base skill
|
28
|
+
# Since lower skill_value is better: Best player gets 0.8x (closer to better levels), worst gets 1.2x
|
29
|
+
skill_adjustment = 1.2 - 0.4 * ppg_skill_factor
|
30
|
+
skill_value = level_skill_value * skill_adjustment
|
31
|
+
|
32
|
+
# Take the minimum skill value across all levels the player has played in (lower is better)
|
33
|
+
min_skill_value = min(min_skill_value, skill_value)
|
34
|
+
|
35
|
+
return min_skill_value if min_skill_value != float('inf') else 0
|
27
36
|
|
28
37
|
def assign_skater_skill_values():
|
29
38
|
session = create_session("boss")
|
30
39
|
|
31
40
|
humans = session.query(Human).all()
|
32
41
|
total_humans = len(humans)
|
33
|
-
|
42
|
+
|
43
|
+
# Create progress tracker
|
44
|
+
progress = create_progress_tracker(total_humans, "Assigning skater skill values")
|
34
45
|
|
35
|
-
for human in humans:
|
46
|
+
for i, human in enumerate(humans):
|
36
47
|
level_stats = session.query(LevelStatsSkater).filter(LevelStatsSkater.human_id == human.id).all()
|
37
48
|
if level_stats:
|
38
|
-
skater_skill_value = calculate_skater_skill_value(session,
|
49
|
+
skater_skill_value = calculate_skater_skill_value(session, level_stats)
|
39
50
|
human.skater_skill_value = skater_skill_value
|
40
51
|
session.commit()
|
41
52
|
|
42
|
-
|
43
|
-
print(f"\rProcessed {processed_humans}/{total_humans} humans ({(processed_humans/total_humans)*100:.2f}%)", end="")
|
53
|
+
progress.update(i + 1)
|
44
54
|
|
45
|
-
print("
|
55
|
+
print("Skater skill values have been assigned to all humans.")
|
46
56
|
|
47
57
|
if __name__ == "__main__":
|
48
58
|
assign_skater_skill_values()
|
@@ -0,0 +1,107 @@
|
|
1
|
+
from hockey_blast_common_lib.models import db
|
2
|
+
|
3
|
+
class H2HStats(db.Model):
|
4
|
+
__tablename__ = 'h2h_stats'
|
5
|
+
id = db.Column(db.Integer, primary_key=True)
|
6
|
+
human1_id = db.Column(db.Integer, db.ForeignKey('humans.id'), nullable=False)
|
7
|
+
human2_id = db.Column(db.Integer, db.ForeignKey('humans.id'), nullable=False)
|
8
|
+
# Always store with human1_id < human2_id for uniqueness
|
9
|
+
__table_args__ = (
|
10
|
+
db.UniqueConstraint('human1_id', 'human2_id', name='_h2h_human_pair_uc'),
|
11
|
+
db.Index('ix_h2h_human_pair', 'human1_id', 'human2_id'), # Composite index for fast lookup
|
12
|
+
)
|
13
|
+
|
14
|
+
# General
|
15
|
+
games_together = db.Column(db.Integer, default=0, nullable=False) # Games where both played (any role, any team)
|
16
|
+
games_against = db.Column(db.Integer, default=0, nullable=False) # Games where both played on opposing teams
|
17
|
+
games_tied_together = db.Column(db.Integer, default=0, nullable=False)
|
18
|
+
games_tied_against = db.Column(db.Integer, default=0, nullable=False) # Games against each other that ended in a tie
|
19
|
+
wins_together = db.Column(db.Integer, default=0, nullable=False) # Games both played on same team and won
|
20
|
+
losses_together = db.Column(db.Integer, default=0, nullable=False) # Games both played on same team and lost
|
21
|
+
h1_wins_vs_h2 = db.Column(db.Integer, default=0, nullable=False) # Games h1's team won vs h2's team
|
22
|
+
h2_wins_vs_h1 = db.Column(db.Integer, default=0, nullable=False) # Games h2's team won vs h1's team
|
23
|
+
|
24
|
+
# Role-specific counts
|
25
|
+
games_h1_goalie = db.Column(db.Integer, default=0, nullable=False) # Games where h1 was a goalie and h2 played
|
26
|
+
games_h2_goalie = db.Column(db.Integer, default=0, nullable=False) # Games where h2 was a goalie and h1 played
|
27
|
+
games_h1_ref = db.Column(db.Integer, default=0, nullable=False) # Games where h1 was a referee and h2 played
|
28
|
+
games_h2_ref = db.Column(db.Integer, default=0, nullable=False) # Games where h2 was a referee and h1 played
|
29
|
+
games_both_referees = db.Column(db.Integer, default=0, nullable=False) # Games where both were referees
|
30
|
+
|
31
|
+
# Goals/Assists/Penalties (when both played)
|
32
|
+
goals_h1_when_together = db.Column(db.Integer, default=0, nullable=False) # Goals by h1 when both played
|
33
|
+
goals_h2_when_together = db.Column(db.Integer, default=0, nullable=False) # Goals by h2 when both played
|
34
|
+
assists_h1_when_together = db.Column(db.Integer, default=0, nullable=False) # Assists by h1 when both played
|
35
|
+
assists_h2_when_together = db.Column(db.Integer, default=0, nullable=False) # Assists by h2 when both played
|
36
|
+
penalties_h1_when_together = db.Column(db.Integer, default=0, nullable=False) # Penalties on h1 when both played
|
37
|
+
penalties_h2_when_together = db.Column(db.Integer, default=0, nullable=False) # Penalties on h2 when both played
|
38
|
+
gm_penalties_h1_when_together = db.Column(db.Integer, default=0, nullable=False) # GM penalties on h1 when both played
|
39
|
+
gm_penalties_h2_when_together = db.Column(db.Integer, default=0, nullable=False) # GM penalties on h2 when both played
|
40
|
+
|
41
|
+
# Goalie/Skater head-to-head (when one is goalie, other is skater on opposing team)
|
42
|
+
h1_goalie_h2_scorer_goals = db.Column(db.Integer, default=0, nullable=False) # Goals scored by h2 against h1 as goalie
|
43
|
+
h2_goalie_h1_scorer_goals = db.Column(db.Integer, default=0, nullable=False) # Goals scored by h1 against h2 as goalie
|
44
|
+
shots_faced_h1_goalie_vs_h2 = db.Column(db.Integer, default=0, nullable=False) # Shots faced by h1 as goalie vs h2 as skater
|
45
|
+
shots_faced_h2_goalie_vs_h1 = db.Column(db.Integer, default=0, nullable=False) # Shots faced by h2 as goalie vs h1 as skater
|
46
|
+
goals_allowed_h1_goalie_vs_h2 = db.Column(db.Integer, default=0, nullable=False) # Goals allowed by h1 as goalie vs h2 as skater
|
47
|
+
goals_allowed_h2_goalie_vs_h1 = db.Column(db.Integer, default=0, nullable=False) # Goals allowed by h2 as goalie vs h1 as skater
|
48
|
+
save_percentage_h1_goalie_vs_h2 = db.Column(db.Float, default=0.0, nullable=False) # Save % by h1 as goalie vs h2 as skater
|
49
|
+
save_percentage_h2_goalie_vs_h1 = db.Column(db.Float, default=0.0, nullable=False) # Save % by h2 as goalie vs h1 as skater
|
50
|
+
|
51
|
+
# Referee/Player
|
52
|
+
h1_ref_h2_player_games = db.Column(db.Integer, default=0, nullable=False) # Games h1 was referee, h2 was player
|
53
|
+
h2_ref_h1_player_games = db.Column(db.Integer, default=0, nullable=False) # Games h2 was referee, h1 was player
|
54
|
+
h1_ref_penalties_on_h2 = db.Column(db.Integer, default=0, nullable=False) # Penalties given by h1 (as ref) to h2
|
55
|
+
h2_ref_penalties_on_h1 = db.Column(db.Integer, default=0, nullable=False) # Penalties given by h2 (as ref) to h1
|
56
|
+
h1_ref_gm_penalties_on_h2 = db.Column(db.Integer, default=0, nullable=False) # GM penalties given by h1 (as ref) to h2
|
57
|
+
h2_ref_gm_penalties_on_h1 = db.Column(db.Integer, default=0, nullable=False) # GM penalties given by h2 (as ref) to h1
|
58
|
+
|
59
|
+
# Both referees (when both are referees in the same game)
|
60
|
+
penalties_given_both_refs = db.Column(db.Integer, default=0, nullable=False) # Total penalties given by both
|
61
|
+
gm_penalties_given_both_refs = db.Column(db.Integer, default=0, nullable=False) # Total GM penalties given by both
|
62
|
+
|
63
|
+
# Shootouts
|
64
|
+
h1_shootout_attempts_vs_h2_goalie = db.Column(db.Integer, default=0, nullable=False) # h1 shootout attempts vs h2 as goalie
|
65
|
+
h1_shootout_goals_vs_h2_goalie = db.Column(db.Integer, default=0, nullable=False) # h1 shootout goals vs h2 as goalie
|
66
|
+
h2_shootout_attempts_vs_h1_goalie = db.Column(db.Integer, default=0, nullable=False) # h2 shootout attempts vs h1 as goalie
|
67
|
+
h2_shootout_goals_vs_h1_goalie = db.Column(db.Integer, default=0, nullable=False) # h2 shootout goals vs h1 as goalie
|
68
|
+
|
69
|
+
# First and last game IDs where both were present
|
70
|
+
first_game_id = db.Column(db.Integer, nullable=False) # Game.id of the first game where both were present
|
71
|
+
last_game_id = db.Column(db.Integer, nullable=False) # Game.id of the most recent game where both were present
|
72
|
+
|
73
|
+
class H2HStatsMeta(db.Model):
|
74
|
+
__tablename__ = 'h2h_stats_meta'
|
75
|
+
id = db.Column(db.Integer, primary_key=True)
|
76
|
+
last_run_timestamp = db.Column(db.DateTime, nullable=True) # When the h2h stats were last updated
|
77
|
+
last_processed_game_id = db.Column(db.Integer, nullable=True) # Game.id of the latest processed game
|
78
|
+
|
79
|
+
class SkaterToSkaterStats(db.Model):
|
80
|
+
__tablename__ = 'skater_to_skater_stats'
|
81
|
+
id = db.Column(db.Integer, primary_key=True)
|
82
|
+
skater1_id = db.Column(db.Integer, db.ForeignKey('humans.id'), nullable=False)
|
83
|
+
skater2_id = db.Column(db.Integer, db.ForeignKey('humans.id'), nullable=False)
|
84
|
+
__table_args__ = (
|
85
|
+
db.UniqueConstraint('skater1_id', 'skater2_id', name='_s2s_skater_pair_uc'),
|
86
|
+
db.Index('ix_s2s_skater_pair', 'skater1_id', 'skater2_id'),
|
87
|
+
)
|
88
|
+
|
89
|
+
# General stats
|
90
|
+
games_against = db.Column(db.Integer, default=0, nullable=False)
|
91
|
+
games_tied_against = db.Column(db.Integer, default=0, nullable=False)
|
92
|
+
skater1_wins_vs_skater2 = db.Column(db.Integer, default=0, nullable=False)
|
93
|
+
skater2_wins_vs_skater1 = db.Column(db.Integer, default=0, nullable=False)
|
94
|
+
|
95
|
+
# Cumulative stats
|
96
|
+
skater1_goals_against_skater2 = db.Column(db.Integer, default=0, nullable=False)
|
97
|
+
skater2_goals_against_skater1 = db.Column(db.Integer, default=0, nullable=False)
|
98
|
+
skater1_assists_against_skater2 = db.Column(db.Integer, default=0, nullable=False)
|
99
|
+
skater2_assists_against_skater1 = db.Column(db.Integer, default=0, nullable=False)
|
100
|
+
skater1_penalties_against_skater2 = db.Column(db.Integer, default=0, nullable=False)
|
101
|
+
skater2_penalties_against_skater1 = db.Column(db.Integer, default=0, nullable=False)
|
102
|
+
|
103
|
+
class SkaterToSkaterStatsMeta(db.Model):
|
104
|
+
__tablename__ = 'skater_to_skater_stats_meta'
|
105
|
+
id = db.Column(db.Integer, primary_key=True)
|
106
|
+
last_run_timestamp = db.Column(db.DateTime, nullable=True)
|
107
|
+
last_processed_game_id = db.Column(db.Integer, nullable=True)
|
Binary file
|
@@ -351,6 +351,7 @@ class RequestLog(db.Model):
|
|
351
351
|
path = db.Column(db.String, nullable=False)
|
352
352
|
timestamp = db.Column(db.DateTime, nullable=False)
|
353
353
|
cgi_params = db.Column(db.String, nullable=True)
|
354
|
+
response_time_ms = db.Column(db.Float, nullable=True) # Response time in milliseconds
|
354
355
|
|
355
356
|
# # MANUAL AMENDS HAPPEN HERE :)
|
356
357
|
# from db_connection import create_session
|
@@ -0,0 +1,91 @@
|
|
1
|
+
import time
|
2
|
+
from datetime import datetime, timedelta
|
3
|
+
|
4
|
+
class ProgressTracker:
|
5
|
+
"""
|
6
|
+
Reusable progress tracker with ETA calculation for stats aggregation processes.
|
7
|
+
"""
|
8
|
+
|
9
|
+
def __init__(self, total_items, description="Processing"):
|
10
|
+
self.total_items = total_items
|
11
|
+
self.description = description
|
12
|
+
self.start_time = time.time()
|
13
|
+
self.processed_items = 0
|
14
|
+
self.last_update_time = self.start_time
|
15
|
+
|
16
|
+
def update(self, processed_count=None):
|
17
|
+
"""
|
18
|
+
Update progress. If processed_count is None, increment by 1.
|
19
|
+
"""
|
20
|
+
if processed_count is not None:
|
21
|
+
self.processed_items = processed_count
|
22
|
+
else:
|
23
|
+
self.processed_items += 1
|
24
|
+
|
25
|
+
current_time = time.time()
|
26
|
+
|
27
|
+
# Only update display if at least 0.1 seconds have passed (to avoid spamming)
|
28
|
+
if current_time - self.last_update_time >= 0.1 or self.processed_items == self.total_items:
|
29
|
+
self._display_progress()
|
30
|
+
self.last_update_time = current_time
|
31
|
+
|
32
|
+
def _display_progress(self):
|
33
|
+
"""
|
34
|
+
Display progress with percentage, ETA, and elapsed time.
|
35
|
+
"""
|
36
|
+
current_time = time.time()
|
37
|
+
elapsed_time = current_time - self.start_time
|
38
|
+
|
39
|
+
if self.processed_items == 0:
|
40
|
+
percentage = 0.0
|
41
|
+
eta_str = "calculating..."
|
42
|
+
else:
|
43
|
+
percentage = (self.processed_items / self.total_items) * 100
|
44
|
+
|
45
|
+
# Calculate ETA
|
46
|
+
if self.processed_items == self.total_items:
|
47
|
+
eta_str = "completed!"
|
48
|
+
else:
|
49
|
+
avg_time_per_item = elapsed_time / self.processed_items
|
50
|
+
remaining_items = self.total_items - self.processed_items
|
51
|
+
eta_seconds = avg_time_per_item * remaining_items
|
52
|
+
eta_str = self._format_time(eta_seconds)
|
53
|
+
|
54
|
+
elapsed_str = self._format_time(elapsed_time)
|
55
|
+
|
56
|
+
progress_msg = f"\r{self.description}: {self.processed_items}/{self.total_items} ({percentage:.1f}%) | "
|
57
|
+
progress_msg += f"Elapsed: {elapsed_str} | ETA: {eta_str}"
|
58
|
+
|
59
|
+
print(progress_msg, end="", flush=True)
|
60
|
+
|
61
|
+
# Add newline when complete
|
62
|
+
if self.processed_items == self.total_items:
|
63
|
+
print() # Newline to finish the progress line
|
64
|
+
|
65
|
+
def _format_time(self, seconds):
|
66
|
+
"""
|
67
|
+
Format seconds into a human-readable string.
|
68
|
+
"""
|
69
|
+
if seconds < 60:
|
70
|
+
return f"{seconds:.1f}s"
|
71
|
+
elif seconds < 3600:
|
72
|
+
minutes = int(seconds // 60)
|
73
|
+
secs = int(seconds % 60)
|
74
|
+
return f"{minutes}m {secs}s"
|
75
|
+
else:
|
76
|
+
hours = int(seconds // 3600)
|
77
|
+
minutes = int((seconds % 3600) // 60)
|
78
|
+
return f"{hours}h {minutes}m"
|
79
|
+
|
80
|
+
def finish(self):
|
81
|
+
"""
|
82
|
+
Mark progress as complete and add final newline.
|
83
|
+
"""
|
84
|
+
self.processed_items = self.total_items
|
85
|
+
self._display_progress()
|
86
|
+
|
87
|
+
def create_progress_tracker(total_items, description="Processing"):
|
88
|
+
"""
|
89
|
+
Factory function to create a progress tracker.
|
90
|
+
"""
|
91
|
+
return ProgressTracker(total_items, description)
|