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.
@@ -16,6 +16,7 @@ from hockey_blast_common_lib.utils import assign_ranks
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
19
20
 
20
21
  def aggregate_goalie_stats(session, aggregation_type, aggregation_id, names_to_filter_out, debug_human_id=None, aggregation_window=None):
21
22
  human_ids_to_filter = get_human_ids_by_names(session, names_to_filter_out)
@@ -190,33 +191,51 @@ def run_aggregate_goalie_stats():
190
191
 
191
192
  for org_id in org_ids:
192
193
  division_ids = get_all_division_ids_for_org(session, org_id)
193
- print(f"Aggregating goalie stats for {len(division_ids)} divisions in org_id {org_id}...")
194
- total_divisions = len(division_ids)
195
- processed_divisions = 0
196
- for division_id in division_ids:
197
- aggregate_goalie_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, debug_human_id=human_id_to_debug)
198
- aggregate_goalie_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')
199
- aggregate_goalie_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')
200
- processed_divisions += 1
201
- if human_id_to_debug is None:
202
- print(f"\rProcessed {processed_divisions}/{total_divisions} divisions ({(processed_divisions/total_divisions)*100:.2f}%)", end="")
203
-
204
- aggregate_goalie_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names, debug_human_id=human_id_to_debug)
205
- aggregate_goalie_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')
206
- aggregate_goalie_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')
194
+ org_name = session.query(Organization.organization_name).filter(Organization.id == org_id).scalar() or f"org_id {org_id}"
207
195
 
208
- # Aggregate by level
209
- level_ids = session.query(Division.level_id).distinct().all()
210
- level_ids = [level_id[0] for level_id in level_ids]
211
- total_levels = len(level_ids)
212
- processed_levels = 0
213
- for level_id in level_ids:
214
- if level_id is None:
215
- continue
196
+ if human_id_to_debug is None and division_ids:
197
+ # Process divisions with progress tracking
198
+ progress = create_progress_tracker(len(division_ids), f"Processing {len(division_ids)} divisions for {org_name}")
199
+ for i, division_id in enumerate(division_ids):
200
+ aggregate_goalie_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, debug_human_id=human_id_to_debug)
201
+ aggregate_goalie_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')
202
+ aggregate_goalie_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')
203
+ progress.update(i + 1)
204
+ else:
205
+ # Debug mode or no divisions - process without progress tracking
206
+ for division_id in division_ids:
207
+ aggregate_goalie_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, debug_human_id=human_id_to_debug)
208
+ aggregate_goalie_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')
209
+ aggregate_goalie_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')
210
+
211
+ # Process org-level stats with progress tracking
216
212
  if human_id_to_debug is None:
217
- print(f"\rProcessed {processed_levels}/{total_levels} levels ({(processed_levels/total_levels)*100:.2f}%)", end="")
218
- processed_levels += 1
219
- aggregate_goalie_stats(session, aggregation_type='level', aggregation_id=level_id, names_to_filter_out=not_human_names, debug_human_id=human_id_to_debug)
213
+ org_progress = create_progress_tracker(3, f"Processing org-level stats for {org_name}")
214
+ aggregate_goalie_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names, debug_human_id=human_id_to_debug)
215
+ org_progress.update(1)
216
+ aggregate_goalie_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')
217
+ org_progress.update(2)
218
+ aggregate_goalie_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')
219
+ org_progress.update(3)
220
+ else:
221
+ aggregate_goalie_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names, debug_human_id=human_id_to_debug)
222
+ aggregate_goalie_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')
223
+ aggregate_goalie_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')
224
+
225
+ # Aggregate by level
226
+ level_ids = session.query(Division.level_id).distinct().all()
227
+ level_ids = [level_id[0] for level_id in level_ids if level_id[0] is not None]
228
+
229
+ if human_id_to_debug is None and level_ids:
230
+ # Process levels with progress tracking
231
+ level_progress = create_progress_tracker(len(level_ids), f"Processing {len(level_ids)} skill levels")
232
+ for i, level_id in enumerate(level_ids):
233
+ aggregate_goalie_stats(session, aggregation_type='level', aggregation_id=level_id, names_to_filter_out=not_human_names, debug_human_id=human_id_to_debug)
234
+ level_progress.update(i + 1)
235
+ else:
236
+ # Debug mode or no levels - process without progress tracking
237
+ for level_id in level_ids:
238
+ aggregate_goalie_stats(session, aggregation_type='level', aggregation_id=level_id, names_to_filter_out=not_human_names, debug_human_id=human_id_to_debug)
220
239
 
221
240
  if __name__ == "__main__":
222
241
  run_aggregate_goalie_stats()
@@ -0,0 +1,231 @@
1
+ import sys, os
2
+ from datetime import datetime
3
+
4
+ # Add the package directory to the Python path
5
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
6
+
7
+ from hockey_blast_common_lib.models import Game, GameRoster, Goal, Penalty
8
+ from hockey_blast_common_lib.h2h_models import H2HStats, H2HStatsMeta
9
+ from hockey_blast_common_lib.db_connection import create_session
10
+ from hockey_blast_common_lib.progress_utils import create_progress_tracker
11
+ from sqlalchemy.sql import func
12
+ from sqlalchemy import types
13
+
14
+ # Max games to process (set to None to process all)
15
+ MAX_GAMES_TO_PROCESS = None # Set to None to process all games
16
+
17
+ def aggregate_h2h_stats():
18
+ session = create_session("boss")
19
+ meta = None #session.query(H2HStatsMeta).order_by(H2HStatsMeta.id.desc()).first()
20
+ h2h_stats_dict = {} # (h1, h2) -> H2HStats instance
21
+ if meta is None or meta.last_run_timestamp is None or meta.last_processed_game_id is None:
22
+ # Full run: delete all existing stats and process all games
23
+ session.query(H2HStats).delete()
24
+ session.commit()
25
+ games_query = session.query(Game).order_by(Game.date, Game.time, Game.id)
26
+ print("No previous run found, deleted all existing H2H stats, processing all games...")
27
+ else:
28
+ # Incremental: only process games after last processed
29
+ # Load all existing stats into memory
30
+ for stat in session.query(H2HStats).all():
31
+ h2h_stats_dict[(stat.human1_id, stat.human2_id)] = stat
32
+ last_game = session.query(Game).filter(Game.id == meta.last_processed_game_id).first()
33
+ if last_game:
34
+ last_dt = datetime.combine(last_game.date, last_game.time)
35
+ games_query = session.query(Game).filter(
36
+ func.cast(func.concat(Game.date, ' ', Game.time), types.TIMESTAMP()) > last_dt
37
+ ).order_by(Game.date, Game.time, Game.id)
38
+ print(f"Resuming from game after id {meta.last_processed_game_id} ({last_dt})...")
39
+ else:
40
+ games_query = session.query(Game).order_by(Game.date, Game.time, Game.id)
41
+ print("Previous game id not found, processing all games...")
42
+
43
+ total_games = games_query.count()
44
+ print(f"Total games to process: {total_games}")
45
+
46
+ # Create progress tracker
47
+ progress = create_progress_tracker(total_games, "Processing H2H stats")
48
+ processed = 0
49
+ latest_game_id = None
50
+ for game in games_query:
51
+ if MAX_GAMES_TO_PROCESS is not None and processed >= MAX_GAMES_TO_PROCESS:
52
+ break
53
+ # --- Gather all relevant data for this game ---
54
+ # Get all GameRoster entries for this game
55
+ rosters = session.query(GameRoster).filter(GameRoster.game_id == game.id).all()
56
+ # Map: team_id -> set of human_ids (players)
57
+ team_to_players = {}
58
+ human_roles = {} # human_id -> set of roles in this game
59
+ for roster in rosters:
60
+ team_to_players.setdefault(roster.team_id, set()).add(roster.human_id)
61
+ human_roles.setdefault(roster.human_id, set()).add(roster.role)
62
+ # Get all human_ids in this game
63
+ all_humans = set(human_roles.keys())
64
+ # Add goalies from Game table (home/visitor)
65
+ if game.home_goalie_id:
66
+ all_humans.add(game.home_goalie_id)
67
+ human_roles.setdefault(game.home_goalie_id, set()).add('G')
68
+ if game.visitor_goalie_id:
69
+ all_humans.add(game.visitor_goalie_id)
70
+ human_roles.setdefault(game.visitor_goalie_id, set()).add('G')
71
+ # Add referees from Game table (NOT from roster!)
72
+ if game.referee_1_id:
73
+ all_humans.add(game.referee_1_id)
74
+ human_roles.setdefault(game.referee_1_id, set()).add('R')
75
+ if game.referee_2_id:
76
+ all_humans.add(game.referee_2_id)
77
+ human_roles.setdefault(game.referee_2_id, set()).add('R')
78
+ # --- Build all pairs of humans in this game ---
79
+ all_humans = list(all_humans)
80
+ for i in range(len(all_humans)):
81
+ for j in range(i+1, len(all_humans)):
82
+ h1, h2 = sorted([all_humans[i], all_humans[j]])
83
+ key = (h1, h2)
84
+ h2h = h2h_stats_dict.get(key)
85
+ if not h2h:
86
+ h2h = H2HStats(
87
+ human1_id=h1,
88
+ human2_id=h2,
89
+ first_game_id=game.id,
90
+ last_game_id=game.id,
91
+ games_together=0,
92
+ games_against=0,
93
+ games_tied_together=0,
94
+ games_tied_against=0,
95
+ wins_together=0,
96
+ losses_together=0,
97
+ h1_wins_vs_h2=0,
98
+ h2_wins_vs_h1=0,
99
+ games_h1_goalie=0,
100
+ games_h2_goalie=0,
101
+ games_h1_ref=0,
102
+ games_h2_ref=0,
103
+ games_both_referees=0,
104
+ goals_h1_when_together=0,
105
+ goals_h2_when_together=0,
106
+ assists_h1_when_together=0,
107
+ assists_h2_when_together=0,
108
+ penalties_h1_when_together=0,
109
+ penalties_h2_when_together=0,
110
+ gm_penalties_h1_when_together=0,
111
+ gm_penalties_h2_when_together=0,
112
+ h1_goalie_h2_scorer_goals=0,
113
+ h2_goalie_h1_scorer_goals=0,
114
+ shots_faced_h1_goalie_vs_h2=0,
115
+ shots_faced_h2_goalie_vs_h1=0,
116
+ goals_allowed_h1_goalie_vs_h2=0,
117
+ goals_allowed_h2_goalie_vs_h1=0,
118
+ save_percentage_h1_goalie_vs_h2=0.0,
119
+ save_percentage_h2_goalie_vs_h1=0.0,
120
+ h1_ref_h2_player_games=0,
121
+ h2_ref_h1_player_games=0,
122
+ h1_ref_penalties_on_h2=0,
123
+ h2_ref_penalties_on_h1=0,
124
+ h1_ref_gm_penalties_on_h2=0,
125
+ h2_ref_gm_penalties_on_h1=0,
126
+ penalties_given_both_refs=0,
127
+ gm_penalties_given_both_refs=0,
128
+ h1_shootout_attempts_vs_h2_goalie=0,
129
+ h1_shootout_goals_vs_h2_goalie=0,
130
+ h2_shootout_attempts_vs_h1_goalie=0,
131
+ h2_shootout_goals_vs_h1_goalie=0
132
+ )
133
+ h2h_stats_dict[key] = h2h
134
+ # Update first/last game ids
135
+ if game.id < h2h.first_game_id:
136
+ h2h.first_game_id = game.id
137
+ if game.id > h2h.last_game_id:
138
+ h2h.last_game_id = game.id
139
+ # --- Determine roles and teams ---
140
+ h1_roles = human_roles.get(h1, set())
141
+ h2_roles = human_roles.get(h2, set())
142
+ h1_team = None
143
+ h2_team = None
144
+ for team_id, players in team_to_players.items():
145
+ if h1 in players:
146
+ h1_team = team_id
147
+ if h2 in players:
148
+ h2_team = team_id
149
+ # --- General stats ---
150
+ h2h.games_together += 1 # Both present in this game
151
+ if h1_team and h2_team:
152
+ if h1_team == h2_team:
153
+ h2h.wins_together += int(_is_win(game, h1_team))
154
+ h2h.losses_together += int(_is_loss(game, h1_team))
155
+ if _is_tie(game):
156
+ h2h.games_tied_together += 1
157
+ else:
158
+ h2h.games_against += 1
159
+ if _is_win(game, h1_team):
160
+ h2h.h1_wins_vs_h2 += 1
161
+ if _is_win(game, h2_team):
162
+ h2h.h2_wins_vs_h1 += 1
163
+ if _is_tie(game):
164
+ h2h.games_tied_against += 1
165
+ # --- Role-specific stats ---
166
+ if 'G' in h1_roles:
167
+ h2h.games_h1_goalie += 1
168
+ if 'G' in h2_roles:
169
+ h2h.games_h2_goalie += 1
170
+ if 'R' in h1_roles:
171
+ h2h.games_h1_ref += 1
172
+ if 'R' in h2_roles:
173
+ h2h.games_h2_ref += 1
174
+ if 'R' in h1_roles and 'R' in h2_roles:
175
+ h2h.games_both_referees += 1
176
+ # --- Goals, assists, penalties ---
177
+ # Goals
178
+ goals = session.query(Goal).filter(Goal.game_id == game.id).all()
179
+ for goal in goals:
180
+ if goal.goal_scorer_id == h1 and (goal.assist_1_id == h2 or goal.assist_2_id == h2):
181
+ h2h.goals_h1_when_together += 1
182
+ if goal.goal_scorer_id == h2 and (goal.assist_1_id == h1 or goal.assist_2_id == h1):
183
+ h2h.goals_h2_when_together += 1
184
+ # Penalties
185
+ penalties = session.query(Penalty).filter(Penalty.game_id == game.id).all()
186
+ for pen in penalties:
187
+ if pen.penalized_player_id == h1:
188
+ h2h.penalties_h1_when_together += 1
189
+ if pen.penalty_minutes and 'GM' in pen.penalty_minutes:
190
+ h2h.gm_penalties_h1_when_together += 1
191
+ if pen.penalized_player_id == h2:
192
+ h2h.penalties_h2_when_together += 1
193
+ if pen.penalty_minutes and 'GM' in pen.penalty_minutes:
194
+ h2h.gm_penalties_h2_when_together += 1
195
+ # --- TODO: Add more detailed logic for goalie/skater, referee/player, shootouts, etc. ---
196
+ latest_game_id = game.id
197
+ processed += 1
198
+ progress.update(processed)
199
+ # Commit all stats at once
200
+ session.query(H2HStats).delete()
201
+ session.add_all(list(h2h_stats_dict.values()))
202
+ session.commit()
203
+ # Save/update meta
204
+ meta = H2HStatsMeta(
205
+ last_run_timestamp=datetime.utcnow(),
206
+ last_processed_game_id=latest_game_id
207
+ )
208
+ session.add(meta)
209
+ session.commit()
210
+ print("H2H aggregation complete.")
211
+
212
+ # --- Helper functions for win/loss/tie ---
213
+ def _is_win(game, team_id):
214
+ if team_id == game.home_team_id:
215
+ return (game.home_final_score or 0) > (game.visitor_final_score or 0)
216
+ if team_id == game.visitor_team_id:
217
+ return (game.visitor_final_score or 0) > (game.home_final_score or 0)
218
+ return False
219
+
220
+ def _is_loss(game, team_id):
221
+ if team_id == game.home_team_id:
222
+ return (game.home_final_score or 0) < (game.visitor_final_score or 0)
223
+ if team_id == game.visitor_team_id:
224
+ return (game.visitor_final_score or 0) < (game.home_final_score or 0)
225
+ return False
226
+
227
+ def _is_tie(game):
228
+ return (game.home_final_score is not None and game.visitor_final_score is not None and game.home_final_score == game.visitor_final_score)
229
+
230
+ if __name__ == "__main__":
231
+ aggregate_h2h_stats()
@@ -15,6 +15,7 @@ from hockey_blast_common_lib.utils import get_fake_human_for_stats, get_org_id_f
15
15
  from hockey_blast_common_lib.utils import assign_ranks
16
16
  from hockey_blast_common_lib.utils import get_start_datetime
17
17
  from hockey_blast_common_lib.stats_utils import ALL_ORGS_ID
18
+ from hockey_blast_common_lib.progress_utils import create_progress_tracker
18
19
 
19
20
  def aggregate_human_stats(session, aggregation_type, aggregation_id, names_to_filter_out, human_id_filter=None, aggregation_window=None):
20
21
  human_ids_to_filter = get_human_ids_by_names(session, names_to_filter_out)
@@ -425,35 +426,53 @@ def run_aggregate_human_stats():
425
426
  org_ids = session.query(Organization.id).all()
426
427
  org_ids = [org_id[0] for org_id in org_ids]
427
428
 
428
- for org_id in [-1]:#org_ids:
429
+ for org_id in org_ids:
429
430
  division_ids = get_all_division_ids_for_org(session, org_id)
430
- print(f"Aggregating human stats for {len(division_ids)} divisions in org_id {org_id}...")
431
- total_divisions = len(division_ids)
432
- processed_divisions = 0
433
- for division_id in division_ids:
434
- aggregate_human_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, human_id_filter=human_id_to_debug)
435
- aggregate_human_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, human_id_filter=human_id_to_debug, aggregation_window='Weekly')
436
- aggregate_human_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, human_id_filter=human_id_to_debug, aggregation_window='Daily')
437
- processed_divisions += 1
438
- if human_id_to_debug is None:
439
- print(f"\rProcessed {processed_divisions}/{total_divisions} divisions ({(processed_divisions/total_divisions)*100:.2f}%)", end="")
440
- print("")
441
- aggregate_human_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names, human_id_filter=human_id_to_debug)
442
- aggregate_human_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names, human_id_filter=human_id_to_debug, aggregation_window='Weekly')
443
- aggregate_human_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names, human_id_filter=human_id_to_debug, aggregation_window='Daily')
431
+ org_name = session.query(Organization.organization_name).filter(Organization.id == org_id).scalar() or f"org_id {org_id}"
444
432
 
445
- # Aggregate by level
446
- level_ids = session.query(Division.level_id).distinct().all()
447
- level_ids = [level_id[0] for level_id in level_ids]
448
- total_levels = len(level_ids)
449
- processed_levels = 0
450
- for level_id in level_ids:
451
- if level_id is None:
452
- continue
433
+ if human_id_to_debug is None and division_ids:
434
+ # Process divisions with progress tracking
435
+ progress = create_progress_tracker(len(division_ids), f"Processing {len(division_ids)} divisions for {org_name}")
436
+ for i, division_id in enumerate(division_ids):
437
+ aggregate_human_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, human_id_filter=human_id_to_debug)
438
+ aggregate_human_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, human_id_filter=human_id_to_debug, aggregation_window='Weekly')
439
+ aggregate_human_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, human_id_filter=human_id_to_debug, aggregation_window='Daily')
440
+ progress.update(i + 1)
441
+ else:
442
+ # Debug mode or no divisions - process without progress tracking
443
+ for division_id in division_ids:
444
+ aggregate_human_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, human_id_filter=human_id_to_debug)
445
+ aggregate_human_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, human_id_filter=human_id_to_debug, aggregation_window='Weekly')
446
+ aggregate_human_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, human_id_filter=human_id_to_debug, aggregation_window='Daily')
447
+
448
+ # Process org-level stats with progress tracking
453
449
  if human_id_to_debug is None:
454
- print(f"\rProcessed {processed_levels}/{total_levels} levels ({(processed_levels/total_levels)*100:.2f}%)", end="")
455
- processed_levels += 1
456
- aggregate_human_stats(session, aggregation_type='level', aggregation_id=level_id, names_to_filter_out=not_human_names, human_id_filter=human_id_to_debug)
450
+ org_progress = create_progress_tracker(3, f"Processing org-level stats for {org_name}")
451
+ aggregate_human_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names, human_id_filter=human_id_to_debug)
452
+ org_progress.update(1)
453
+ aggregate_human_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names, human_id_filter=human_id_to_debug, aggregation_window='Weekly')
454
+ org_progress.update(2)
455
+ aggregate_human_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names, human_id_filter=human_id_to_debug, aggregation_window='Daily')
456
+ org_progress.update(3)
457
+ else:
458
+ aggregate_human_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names, human_id_filter=human_id_to_debug)
459
+ aggregate_human_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names, human_id_filter=human_id_to_debug, aggregation_window='Weekly')
460
+ aggregate_human_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names, human_id_filter=human_id_to_debug, aggregation_window='Daily')
461
+
462
+ # Aggregate by level
463
+ level_ids = session.query(Division.level_id).distinct().all()
464
+ level_ids = [level_id[0] for level_id in level_ids if level_id[0] is not None]
465
+
466
+ if human_id_to_debug is None and level_ids:
467
+ # Process levels with progress tracking
468
+ level_progress = create_progress_tracker(len(level_ids), f"Processing {len(level_ids)} skill levels")
469
+ for i, level_id in enumerate(level_ids):
470
+ aggregate_human_stats(session, aggregation_type='level', aggregation_id=level_id, names_to_filter_out=not_human_names, human_id_filter=human_id_to_debug)
471
+ level_progress.update(i + 1)
472
+ else:
473
+ # Debug mode or no levels - process without progress tracking
474
+ for level_id in level_ids:
475
+ aggregate_human_stats(session, aggregation_type='level', aggregation_id=level_id, names_to_filter_out=not_human_names, human_id_filter=human_id_to_debug)
457
476
 
458
477
  if __name__ == "__main__":
459
478
  run_aggregate_human_stats()
@@ -15,6 +15,7 @@ from hockey_blast_common_lib.utils import get_org_id_from_alias, get_human_ids_b
15
15
  from hockey_blast_common_lib.utils import assign_ranks
16
16
  from hockey_blast_common_lib.utils import get_start_datetime
17
17
  from hockey_blast_common_lib.stats_utils import ALL_ORGS_ID
18
+ from hockey_blast_common_lib.progress_utils import create_progress_tracker
18
19
 
19
20
  def aggregate_referee_stats(session, aggregation_type, aggregation_id, names_to_filter_out, aggregation_window=None):
20
21
  human_ids_to_filter = get_human_ids_by_names(session, names_to_filter_out)
@@ -214,33 +215,51 @@ def run_aggregate_referee_stats():
214
215
 
215
216
  for org_id in org_ids:
216
217
  division_ids = get_all_division_ids_for_org(session, org_id)
217
- print(f"Aggregating referee stats for {len(division_ids)} divisions in org_id {org_id}...")
218
- total_divisions = len(division_ids)
219
- processed_divisions = 0
220
- for division_id in division_ids:
221
- aggregate_referee_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names)
222
- aggregate_referee_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, aggregation_window='Weekly')
223
- aggregate_referee_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, aggregation_window='Daily')
224
- processed_divisions += 1
225
- if human_id_to_debug is None:
226
- print(f"\rProcessed {processed_divisions}/{total_divisions} divisions ({(processed_divisions/total_divisions)*100:.2f}%)", end="")
227
-
228
- aggregate_referee_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names)
229
- aggregate_referee_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names, aggregation_window='Weekly')
230
- aggregate_referee_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names, aggregation_window='Daily')
218
+ org_name = session.query(Organization.organization_name).filter(Organization.id == org_id).scalar() or f"org_id {org_id}"
231
219
 
232
- # Aggregate by level
233
- level_ids = session.query(Division.level_id).distinct().all()
234
- level_ids = [level_id[0] for level_id in level_ids]
235
- total_levels = len(level_ids)
236
- processed_levels = 0
237
- for level_id in level_ids:
238
- if level_id is None:
239
- continue
220
+ if human_id_to_debug is None and division_ids:
221
+ # Process divisions with progress tracking
222
+ progress = create_progress_tracker(len(division_ids), f"Processing {len(division_ids)} divisions for {org_name}")
223
+ for i, division_id in enumerate(division_ids):
224
+ aggregate_referee_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names)
225
+ aggregate_referee_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, aggregation_window='Weekly')
226
+ aggregate_referee_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, aggregation_window='Daily')
227
+ progress.update(i + 1)
228
+ else:
229
+ # Debug mode or no divisions - process without progress tracking
230
+ for division_id in division_ids:
231
+ aggregate_referee_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names)
232
+ aggregate_referee_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, aggregation_window='Weekly')
233
+ aggregate_referee_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, aggregation_window='Daily')
234
+
235
+ # Process org-level stats with progress tracking
240
236
  if human_id_to_debug is None:
241
- print(f"\rProcessed {processed_levels}/{total_levels} levels ({(processed_levels/total_levels)*100:.2f}%)", end="")
242
- processed_levels += 1
243
- aggregate_referee_stats(session, aggregation_type='level', aggregation_id=level_id, names_to_filter_out=not_human_names)
237
+ org_progress = create_progress_tracker(3, f"Processing org-level stats for {org_name}")
238
+ aggregate_referee_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names)
239
+ org_progress.update(1)
240
+ aggregate_referee_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names, aggregation_window='Weekly')
241
+ org_progress.update(2)
242
+ aggregate_referee_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names, aggregation_window='Daily')
243
+ org_progress.update(3)
244
+ else:
245
+ aggregate_referee_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names)
246
+ aggregate_referee_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names, aggregation_window='Weekly')
247
+ aggregate_referee_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names, aggregation_window='Daily')
248
+
249
+ # Aggregate by level
250
+ level_ids = session.query(Division.level_id).distinct().all()
251
+ level_ids = [level_id[0] for level_id in level_ids if level_id[0] is not None]
252
+
253
+ if human_id_to_debug is None and level_ids:
254
+ # Process levels with progress tracking
255
+ level_progress = create_progress_tracker(len(level_ids), f"Processing {len(level_ids)} skill levels")
256
+ for i, level_id in enumerate(level_ids):
257
+ aggregate_referee_stats(session, aggregation_type='level', aggregation_id=level_id, names_to_filter_out=not_human_names)
258
+ level_progress.update(i + 1)
259
+ else:
260
+ # Debug mode or no levels - process without progress tracking
261
+ for level_id in level_ids:
262
+ aggregate_referee_stats(session, aggregation_type='level', aggregation_id=level_id, names_to_filter_out=not_human_names)
244
263
 
245
264
  if __name__ == "__main__":
246
265
  run_aggregate_referee_stats()
@@ -0,0 +1,143 @@
1
+ import sys, os
2
+ from datetime import datetime
3
+
4
+ # Add the package directory to the Python path
5
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
6
+
7
+ from hockey_blast_common_lib.models import Game, Goal, Penalty, GameRoster
8
+ from hockey_blast_common_lib.h2h_models import SkaterToSkaterStats, SkaterToSkaterStatsMeta
9
+ from hockey_blast_common_lib.db_connection import create_session
10
+ from sqlalchemy.sql import func
11
+ from sqlalchemy import types
12
+
13
+ # Optional: Limit processing to a specific human_id
14
+ LIMIT_HUMAN_ID = None
15
+
16
+ def aggregate_s2s_stats():
17
+ session = create_session("boss")
18
+ meta = session.query(SkaterToSkaterStatsMeta).order_by(SkaterToSkaterStatsMeta.id.desc()).first()
19
+ s2s_stats_dict = {} # (skater1_id, skater2_id) -> SkaterToSkaterStats instance
20
+
21
+ if meta is None or meta.last_run_timestamp is None or meta.last_processed_game_id is None:
22
+ # Full run: delete all existing stats and process all games
23
+ session.query(SkaterToSkaterStats).delete()
24
+ session.commit()
25
+ games_query = session.query(Game).order_by(Game.date, Game.time, Game.id)
26
+ print("No previous run found, deleted all existing Skater-to-Skater stats, processing all games...")
27
+ else:
28
+ # Incremental: only process games after last processed
29
+ for stat in session.query(SkaterToSkaterStats).all():
30
+ s2s_stats_dict[(stat.skater1_id, stat.skater2_id)] = stat
31
+ last_game = session.query(Game).filter(Game.id == meta.last_processed_game_id).first()
32
+ if last_game:
33
+ last_dt = datetime.combine(last_game.date, last_game.time)
34
+ games_query = session.query(Game).filter(
35
+ func.cast(func.concat(Game.date, ' ', Game.time), types.TIMESTAMP()) > last_dt
36
+ ).order_by(Game.date, Game.time, Game.id)
37
+ print(f"Resuming from game after id {meta.last_processed_game_id} ({last_dt})...")
38
+ else:
39
+ games_query = session.query(Game).order_by(Game.date, Game.time, Game.id)
40
+ print("Previous game id not found, processing all games...")
41
+
42
+ total_games = games_query.count()
43
+ print(f"Total games to process: {total_games}")
44
+ processed = 0
45
+ latest_game_id = None
46
+
47
+ for game in games_query:
48
+ # Separate skaters into home and away rosters (exclude goalies)
49
+ home_skaters = [entry.human_id for entry in session.query(GameRoster).filter(GameRoster.game_id == game.id, GameRoster.team_id == game.home_team_id, ~GameRoster.role.ilike('g')).all()]
50
+ away_skaters = [entry.human_id for entry in session.query(GameRoster).filter(GameRoster.game_id == game.id, GameRoster.team_id == game.visitor_team_id, ~GameRoster.role.ilike('g')).all()]
51
+
52
+ if LIMIT_HUMAN_ID is not None and LIMIT_HUMAN_ID not in home_skaters + away_skaters:
53
+ continue
54
+
55
+ # Create pairs of skaters from different rosters
56
+ for h_skater in home_skaters:
57
+ for a_skater in away_skaters:
58
+ if LIMIT_HUMAN_ID is not None and LIMIT_HUMAN_ID not in [h_skater, a_skater]:
59
+ continue
60
+
61
+ s1, s2 = sorted([h_skater, a_skater])
62
+ key = (s1, s2)
63
+ s2s = s2s_stats_dict.get(key)
64
+ if not s2s:
65
+ s2s = SkaterToSkaterStats(
66
+ skater1_id=s1,
67
+ skater2_id=s2,
68
+ games_against=0,
69
+ games_tied_against=0,
70
+ skater1_wins_vs_skater2=0,
71
+ skater2_wins_vs_skater1=0,
72
+ skater1_goals_against_skater2=0,
73
+ skater2_goals_against_skater1=0,
74
+ skater1_assists_against_skater2=0,
75
+ skater2_assists_against_skater1=0,
76
+ skater1_penalties_against_skater2=0,
77
+ skater2_penalties_against_skater1=0
78
+ )
79
+ s2s_stats_dict[key] = s2s
80
+
81
+ # Update stats
82
+ s2s.games_against += 1
83
+ if _is_tie(game):
84
+ s2s.games_tied_against += 1
85
+ elif _is_win(game, s1, game.home_team_id):
86
+ s2s.skater1_wins_vs_skater2 += 1
87
+ elif _is_win(game, s2, game.visitor_team_id):
88
+ s2s.skater2_wins_vs_skater1 += 1
89
+
90
+ # Goals and assists
91
+ goals_stats = session.query(Goal).filter(Goal.game_id == game.id).all()
92
+ for goal in goals_stats:
93
+ if goal.goal_scorer_id == s1:
94
+ s2s.skater1_goals_against_skater2 += 1
95
+ if goal.goal_scorer_id == s2:
96
+ s2s.skater2_goals_against_skater1 += 1
97
+ if goal.assist_1_id == s1 or goal.assist_2_id == s1:
98
+ s2s.skater1_assists_against_skater2 += 1
99
+ if goal.assist_1_id == s2 or goal.assist_2_id == s2:
100
+ s2s.skater2_assists_against_skater1 += 1
101
+
102
+ # Penalties
103
+ penalties_stats = session.query(Penalty).filter(Penalty.game_id == game.id).all()
104
+ for penalty in penalties_stats:
105
+ if penalty.penalized_player_id == s1:
106
+ s2s.skater1_penalties_against_skater2 += 1
107
+ if penalty.penalized_player_id == s2:
108
+ s2s.skater2_penalties_against_skater1 += 1
109
+
110
+ latest_game_id = game.id
111
+ processed += 1
112
+ if processed % 10 == 0 or processed == total_games:
113
+ print(f"\rProcessed {processed}/{total_games} games ({(processed/total_games)*100:.2f}%)", end="")
114
+ sys.stdout.flush()
115
+
116
+ # Commit all stats at once
117
+ session.query(SkaterToSkaterStats).delete()
118
+ session.add_all(list(s2s_stats_dict.values()))
119
+ session.commit()
120
+ print(f"\rProcessed {processed}/{total_games} games (100.00%)")
121
+
122
+ # Save/update meta
123
+ meta = SkaterToSkaterStatsMeta(
124
+ last_run_timestamp=datetime.utcnow(),
125
+ last_processed_game_id=latest_game_id
126
+ )
127
+ session.add(meta)
128
+ session.commit()
129
+ print("Skater-to-Skater aggregation complete.")
130
+
131
+ # --- Helper functions for win/loss/tie ---
132
+ def _is_win(game, skater_id, team_id):
133
+ if team_id == game.home_team_id:
134
+ return (game.home_final_score or 0) > (game.visitor_final_score or 0)
135
+ if team_id == game.visitor_team_id:
136
+ return (game.visitor_final_score or 0) > (game.home_final_score or 0)
137
+ return False
138
+
139
+ def _is_tie(game):
140
+ return (game.home_final_score is not None and game.visitor_final_score is not None and game.home_final_score == game.visitor_final_score)
141
+
142
+ if __name__ == "__main__":
143
+ aggregate_s2s_stats()