hockey-blast-common-lib 0.1.61__py3-none-any.whl → 0.1.62__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.
@@ -9,13 +9,19 @@ import sqlalchemy
9
9
  from hockey_blast_common_lib.models import Game, Organization, Division, Human, GoalieSaves
10
10
  from hockey_blast_common_lib.stats_models import OrgStatsGoalie, DivisionStatsGoalie, OrgStatsWeeklyGoalie, OrgStatsDailyGoalie, DivisionStatsWeeklyGoalie, DivisionStatsDailyGoalie, LevelStatsGoalie
11
11
  from hockey_blast_common_lib.db_connection import create_session
12
- from sqlalchemy.sql import func
12
+ from sqlalchemy.sql import func, case
13
13
  from hockey_blast_common_lib.options import MIN_GAMES_FOR_ORG_STATS, MIN_GAMES_FOR_DIVISION_STATS, MIN_GAMES_FOR_LEVEL_STATS
14
14
  from hockey_blast_common_lib.utils import get_non_human_ids, get_all_division_ids_for_org, get_start_datetime
15
15
  from hockey_blast_common_lib.utils import assign_ranks
16
16
  from hockey_blast_common_lib.stats_utils import ALL_ORGS_ID
17
17
  from hockey_blast_common_lib.progress_utils import create_progress_tracker
18
18
 
19
+ # Import status constants for game filtering
20
+ FINAL_STATUS = "Final"
21
+ FINAL_SO_STATUS = "Final(SO)"
22
+ FORFEIT_STATUS = "FORFEIT"
23
+ NOEVENTS_STATUS = "NOEVENTS"
24
+
19
25
  def aggregate_goalie_stats(session, aggregation_type, aggregation_id, debug_human_id=None, aggregation_window=None):
20
26
  human_ids_to_filter = get_non_human_ids(session)
21
27
 
@@ -72,9 +78,19 @@ def aggregate_goalie_stats(session, aggregation_type, aggregation_id, debug_huma
72
78
 
73
79
 
74
80
  # Aggregate games played, goals allowed, and shots faced for each goalie using GoalieSaves table
81
+ # games_participated: Count FINAL, FINAL_SO, FORFEIT, NOEVENTS
82
+ # games_with_stats: Count only FINAL, FINAL_SO (for per-game averages)
75
83
  query = session.query(
76
84
  GoalieSaves.goalie_id.label('human_id'),
77
- func.count(GoalieSaves.game_id).label('games_played'),
85
+ func.count(GoalieSaves.game_id).label('games_played'), # DEPRECATED - will be replaced by games_participated
86
+ func.sum(case(
87
+ (Game.status.in_([FINAL_STATUS, FINAL_SO_STATUS, FORFEIT_STATUS, NOEVENTS_STATUS]), 1),
88
+ else_=0
89
+ )).label('games_participated'),
90
+ func.sum(case(
91
+ (Game.status.in_([FINAL_STATUS, FINAL_SO_STATUS]), 1),
92
+ else_=0
93
+ )).label('games_with_stats'),
78
94
  func.sum(GoalieSaves.goals_allowed).label('goals_allowed'),
79
95
  func.sum(GoalieSaves.shots_against).label('shots_faced'),
80
96
  func.array_agg(GoalieSaves.game_id).label('game_ids')
@@ -94,7 +110,9 @@ def aggregate_goalie_stats(session, aggregation_type, aggregation_id, debug_huma
94
110
  key = (aggregation_id, stat.human_id)
95
111
  if key not in stats_dict:
96
112
  stats_dict[key] = {
97
- 'games_played': 0,
113
+ 'games_played': 0, # DEPRECATED - for backward compatibility
114
+ 'games_participated': 0, # Total games: FINAL, FINAL_SO, FORFEIT, NOEVENTS
115
+ 'games_with_stats': 0, # Games with full stats: FINAL, FINAL_SO only
98
116
  'goals_allowed': 0,
99
117
  'shots_faced': 0,
100
118
  'goals_allowed_per_game': 0.0,
@@ -104,6 +122,8 @@ def aggregate_goalie_stats(session, aggregation_type, aggregation_id, debug_huma
104
122
  'last_game_id': None
105
123
  }
106
124
  stats_dict[key]['games_played'] += stat.games_played
125
+ stats_dict[key]['games_participated'] += stat.games_participated
126
+ stats_dict[key]['games_with_stats'] += stat.games_with_stats
107
127
  stats_dict[key]['goals_allowed'] += stat.goals_allowed if stat.goals_allowed is not None else 0
108
128
  stats_dict[key]['shots_faced'] += stat.shots_faced if stat.shots_faced is not None else 0
109
129
  stats_dict[key]['game_ids'].extend(stat.game_ids)
@@ -111,10 +131,10 @@ def aggregate_goalie_stats(session, aggregation_type, aggregation_id, debug_huma
111
131
  # Filter out entries with games_played less than min_games
112
132
  stats_dict = {key: value for key, value in stats_dict.items() if value['games_played'] >= min_games}
113
133
 
114
- # Calculate per game stats
134
+ # Calculate per game stats (using games_with_stats as denominator for accuracy)
115
135
  for key, stat in stats_dict.items():
116
- if stat['games_played'] > 0:
117
- stat['goals_allowed_per_game'] = stat['goals_allowed'] / stat['games_played']
136
+ if stat['games_with_stats'] > 0:
137
+ stat['goals_allowed_per_game'] = stat['goals_allowed'] / stat['games_with_stats']
118
138
  stat['save_percentage'] = (stat['shots_faced'] - stat['goals_allowed']) / stat['shots_faced'] if stat['shots_faced'] > 0 else 0.0
119
139
 
120
140
  # Ensure all keys have valid human_id values
@@ -134,6 +154,8 @@ def aggregate_goalie_stats(session, aggregation_type, aggregation_id, debug_huma
134
154
 
135
155
  # Assign ranks within each level
136
156
  assign_ranks(stats_dict, 'games_played')
157
+ assign_ranks(stats_dict, 'games_participated') # Rank by total participation
158
+ assign_ranks(stats_dict, 'games_with_stats') # Rank by games with full stats
137
159
  assign_ranks(stats_dict, 'goals_allowed', reverse_rank=True)
138
160
  assign_ranks(stats_dict, 'shots_faced')
139
161
  assign_ranks(stats_dict, 'goals_allowed_per_game', reverse_rank=True)
@@ -159,7 +181,11 @@ def aggregate_goalie_stats(session, aggregation_type, aggregation_id, debug_huma
159
181
  goalie_stat = StatsModel(
160
182
  aggregation_id=aggregation_id,
161
183
  human_id=human_id,
162
- games_played=stat['games_played'],
184
+ games_played=stat['games_played'], # DEPRECATED - for backward compatibility
185
+ games_participated=stat['games_participated'], # Total games: FINAL, FINAL_SO, FORFEIT, NOEVENTS
186
+ games_participated_rank=stat['games_participated_rank'],
187
+ games_with_stats=stat['games_with_stats'], # Games with full stats: FINAL, FINAL_SO only
188
+ games_with_stats_rank=stat['games_with_stats_rank'],
163
189
  goals_allowed=stat['goals_allowed'],
164
190
  shots_faced=stat['shots_faced'],
165
191
  goals_allowed_per_game=goals_allowed_per_game,
@@ -17,6 +17,12 @@ from hockey_blast_common_lib.utils import get_start_datetime
17
17
  from hockey_blast_common_lib.stats_utils import ALL_ORGS_ID
18
18
  from hockey_blast_common_lib.progress_utils import create_progress_tracker
19
19
 
20
+ # Import status constants for game filtering
21
+ FINAL_STATUS = "Final"
22
+ FINAL_SO_STATUS = "Final(SO)"
23
+ FORFEIT_STATUS = "FORFEIT"
24
+ NOEVENTS_STATUS = "NOEVENTS"
25
+
20
26
  def aggregate_referee_stats(session, aggregation_type, aggregation_id, aggregation_window=None):
21
27
  human_ids_to_filter = get_non_human_ids(session)
22
28
 
@@ -72,15 +78,33 @@ def aggregate_referee_stats(session, aggregation_type, aggregation_id, aggregati
72
78
 
73
79
  filter_condition = filter_condition & (Division.id == Game.division_id)
74
80
  # Aggregate games reffed for each referee
81
+ # games_participated: Count FINAL, FINAL_SO, FORFEIT, NOEVENTS
82
+ # games_with_stats: Count only FINAL, FINAL_SO (for per-game averages)
75
83
  games_reffed_stats = session.query(
76
84
  Game.referee_1_id.label('human_id'),
77
- func.count(Game.id).label('games_reffed'),
85
+ func.count(Game.id).label('games_reffed'), # DEPRECATED - will be replaced by games_participated
86
+ func.sum(case(
87
+ (Game.status.in_([FINAL_STATUS, FINAL_SO_STATUS, FORFEIT_STATUS, NOEVENTS_STATUS]), 1),
88
+ else_=0
89
+ )).label('games_participated'),
90
+ func.sum(case(
91
+ (Game.status.in_([FINAL_STATUS, FINAL_SO_STATUS]), 1),
92
+ else_=0
93
+ )).label('games_with_stats'),
78
94
  func.array_agg(Game.id).label('game_ids')
79
95
  ).filter(filter_condition).group_by(Game.referee_1_id).all()
80
96
 
81
97
  games_reffed_stats_2 = session.query(
82
98
  Game.referee_2_id.label('human_id'),
83
- func.count(Game.id).label('games_reffed'),
99
+ func.count(Game.id).label('games_reffed'), # DEPRECATED - will be replaced by games_participated
100
+ func.sum(case(
101
+ (Game.status.in_([FINAL_STATUS, FINAL_SO_STATUS, FORFEIT_STATUS, NOEVENTS_STATUS]), 1),
102
+ else_=0
103
+ )).label('games_participated'),
104
+ func.sum(case(
105
+ (Game.status.in_([FINAL_STATUS, FINAL_SO_STATUS]), 1),
106
+ else_=0
107
+ )).label('games_with_stats'),
84
108
  func.array_agg(Game.id).label('game_ids')
85
109
  ).filter(filter_condition).group_by(Game.referee_2_id).all()
86
110
 
@@ -101,7 +125,9 @@ def aggregate_referee_stats(session, aggregation_type, aggregation_id, aggregati
101
125
  key = (aggregation_id, stat.human_id)
102
126
  if key not in stats_dict:
103
127
  stats_dict[key] = {
104
- 'games_reffed': 0,
128
+ 'games_reffed': 0, # DEPRECATED - for backward compatibility
129
+ 'games_participated': 0, # Total games: FINAL, FINAL_SO, FORFEIT, NOEVENTS
130
+ 'games_with_stats': 0, # Games with full stats: FINAL, FINAL_SO only
105
131
  'penalties_given': 0,
106
132
  'gm_given': 0,
107
133
  'penalties_per_game': 0.0,
@@ -111,6 +137,8 @@ def aggregate_referee_stats(session, aggregation_type, aggregation_id, aggregati
111
137
  'last_game_id': None
112
138
  }
113
139
  stats_dict[key]['games_reffed'] += stat.games_reffed
140
+ stats_dict[key]['games_participated'] += stat.games_participated
141
+ stats_dict[key]['games_with_stats'] += stat.games_with_stats
114
142
  stats_dict[key]['game_ids'].extend(stat.game_ids)
115
143
 
116
144
  for stat in games_reffed_stats_2:
@@ -119,7 +147,9 @@ def aggregate_referee_stats(session, aggregation_type, aggregation_id, aggregati
119
147
  key = (aggregation_id, stat.human_id)
120
148
  if key not in stats_dict:
121
149
  stats_dict[key] = {
122
- 'games_reffed': 0,
150
+ 'games_reffed': 0, # DEPRECATED - for backward compatibility
151
+ 'games_participated': 0, # Total games: FINAL, FINAL_SO, FORFEIT, NOEVENTS
152
+ 'games_with_stats': 0, # Games with full stats: FINAL, FINAL_SO only
123
153
  'penalties_given': 0,
124
154
  'gm_given': 0,
125
155
  'penalties_per_game': 0.0,
@@ -129,6 +159,8 @@ def aggregate_referee_stats(session, aggregation_type, aggregation_id, aggregati
129
159
  'last_game_id': None
130
160
  }
131
161
  stats_dict[key]['games_reffed'] += stat.games_reffed
162
+ stats_dict[key]['games_participated'] += stat.games_participated
163
+ stats_dict[key]['games_with_stats'] += stat.games_with_stats
132
164
  stats_dict[key]['game_ids'].extend(stat.game_ids)
133
165
 
134
166
  # Filter out entries with games_reffed less than min_games
@@ -149,11 +181,11 @@ def aggregate_referee_stats(session, aggregation_type, aggregation_id, aggregati
149
181
  stats_dict[key]['gm_given'] += stat.gm_given / 2
150
182
  stats_dict[key]['game_ids'].append(stat.game_id)
151
183
 
152
- # Calculate per game stats
184
+ # Calculate per game stats (using games_with_stats as denominator for accuracy)
153
185
  for key, stat in stats_dict.items():
154
- if stat['games_reffed'] > 0:
155
- stat['penalties_per_game'] = stat['penalties_given'] / stat['games_reffed']
156
- stat['gm_per_game'] = stat['gm_given'] / stat['games_reffed']
186
+ if stat['games_with_stats'] > 0:
187
+ stat['penalties_per_game'] = stat['penalties_given'] / stat['games_with_stats']
188
+ stat['gm_per_game'] = stat['gm_given'] / stat['games_with_stats']
157
189
 
158
190
  # Ensure all keys have valid human_id values
159
191
  stats_dict = {key: value for key, value in stats_dict.items() if key[1] is not None}
@@ -172,6 +204,8 @@ def aggregate_referee_stats(session, aggregation_type, aggregation_id, aggregati
172
204
 
173
205
  # Assign ranks
174
206
  assign_ranks(stats_dict, 'games_reffed')
207
+ assign_ranks(stats_dict, 'games_participated') # Rank by total participation
208
+ assign_ranks(stats_dict, 'games_with_stats') # Rank by games with full stats
175
209
  assign_ranks(stats_dict, 'penalties_given')
176
210
  assign_ranks(stats_dict, 'penalties_per_game')
177
211
  assign_ranks(stats_dict, 'gm_given')
@@ -185,7 +219,11 @@ def aggregate_referee_stats(session, aggregation_type, aggregation_id, aggregati
185
219
  referee_stat = StatsModel(
186
220
  aggregation_id=aggregation_id,
187
221
  human_id=human_id,
188
- games_reffed=stat['games_reffed'],
222
+ games_reffed=stat['games_reffed'], # DEPRECATED - for backward compatibility
223
+ games_participated=stat['games_participated'], # Total games: FINAL, FINAL_SO, FORFEIT, NOEVENTS
224
+ games_participated_rank=stat['games_participated_rank'],
225
+ games_with_stats=stat['games_with_stats'], # Games with full stats: FINAL, FINAL_SO only
226
+ games_with_stats_rank=stat['games_with_stats_rank'],
189
227
  penalties_given=stat['penalties_given'],
190
228
  penalties_per_game=stat['penalties_per_game'],
191
229
  gm_given=stat['gm_given'],
@@ -17,6 +17,12 @@ from hockey_blast_common_lib.utils import get_start_datetime
17
17
  from hockey_blast_common_lib.stats_utils import ALL_ORGS_ID
18
18
  from hockey_blast_common_lib.progress_utils import create_progress_tracker
19
19
 
20
+ # Import status constants for game filtering
21
+ FINAL_STATUS = "Final"
22
+ FINAL_SO_STATUS = "Final(SO)"
23
+ FORFEIT_STATUS = "FORFEIT"
24
+ NOEVENTS_STATUS = "NOEVENTS"
25
+
20
26
  def calculate_quality_score(avg_max_saves_5sec, avg_max_saves_20sec, peak_max_saves_5sec, peak_max_saves_20sec):
21
27
  """
22
28
  Calculate a quality score based on excessive clicking patterns.
@@ -89,9 +95,19 @@ def aggregate_scorekeeper_stats(session, aggregation_type, aggregation_id, aggre
89
95
 
90
96
 
91
97
  # Aggregate scorekeeper quality data for each human
98
+ # games_participated: Count FINAL, FINAL_SO, FORFEIT, NOEVENTS
99
+ # games_with_stats: Count only FINAL, FINAL_SO (for per-game averages)
92
100
  scorekeeper_quality_stats = session.query(
93
101
  ScorekeeperSaveQuality.scorekeeper_id.label('human_id'),
94
- func.count(ScorekeeperSaveQuality.game_id).label('games_recorded'),
102
+ func.count(ScorekeeperSaveQuality.game_id).label('games_recorded'), # DEPRECATED - will be replaced by games_participated
103
+ func.sum(case(
104
+ (Game.status.in_([FINAL_STATUS, FINAL_SO_STATUS, FORFEIT_STATUS, NOEVENTS_STATUS]), 1),
105
+ else_=0
106
+ )).label('games_participated'),
107
+ func.sum(case(
108
+ (Game.status.in_([FINAL_STATUS, FINAL_SO_STATUS]), 1),
109
+ else_=0
110
+ )).label('games_with_stats'),
95
111
  func.sum(ScorekeeperSaveQuality.total_saves_recorded).label('total_saves_recorded'),
96
112
  func.avg(ScorekeeperSaveQuality.total_saves_recorded).label('avg_saves_per_game'),
97
113
  func.avg(ScorekeeperSaveQuality.max_saves_per_5sec).label('avg_max_saves_per_5sec'),
@@ -120,7 +136,9 @@ def aggregate_scorekeeper_stats(session, aggregation_type, aggregation_id, aggre
120
136
  )
121
137
 
122
138
  stats_dict[key] = {
123
- 'games_recorded': stat.games_recorded,
139
+ 'games_recorded': stat.games_recorded, # DEPRECATED - for backward compatibility
140
+ 'games_participated': stat.games_participated, # Total games: FINAL, FINAL_SO, FORFEIT, NOEVENTS
141
+ 'games_with_stats': stat.games_with_stats, # Games with full stats: FINAL, FINAL_SO only
124
142
  'sog_given': stat.total_saves_recorded, # Legacy field name mapping
125
143
  'sog_per_game': stat.avg_saves_per_game or 0.0, # Legacy field name mapping
126
144
  'total_saves_recorded': stat.total_saves_recorded,
@@ -152,6 +170,8 @@ def aggregate_scorekeeper_stats(session, aggregation_type, aggregation_id, aggre
152
170
 
153
171
  # Assign ranks - note: for quality metrics, lower values are better (reverse_rank=True for avg and peak clicking)
154
172
  assign_ranks(stats_dict, 'games_recorded')
173
+ assign_ranks(stats_dict, 'games_participated') # Rank by total participation
174
+ assign_ranks(stats_dict, 'games_with_stats') # Rank by games with full stats
155
175
  assign_ranks(stats_dict, 'sog_given') # Legacy field
156
176
  assign_ranks(stats_dict, 'sog_per_game') # Legacy field
157
177
  assign_ranks(stats_dict, 'total_saves_recorded')
@@ -169,7 +189,11 @@ def aggregate_scorekeeper_stats(session, aggregation_type, aggregation_id, aggre
169
189
  scorekeeper_stat = StatsModel(
170
190
  aggregation_id=aggregation_id,
171
191
  human_id=human_id,
172
- games_recorded=stat['games_recorded'],
192
+ games_recorded=stat['games_recorded'], # DEPRECATED - for backward compatibility
193
+ games_participated=stat['games_participated'], # Total games: FINAL, FINAL_SO, FORFEIT, NOEVENTS
194
+ games_participated_rank=stat['games_participated_rank'],
195
+ games_with_stats=stat['games_with_stats'], # Games with full stats: FINAL, FINAL_SO only
196
+ games_with_stats_rank=stat['games_with_stats_rank'],
173
197
  sog_given=stat['sog_given'], # Legacy field mapping
174
198
  sog_per_game=stat['sog_per_game'], # Legacy field mapping
175
199
  total_saves_recorded=stat['total_saves_recorded'],
@@ -18,6 +18,12 @@ from collections import defaultdict
18
18
  from hockey_blast_common_lib.stats_utils import ALL_ORGS_ID
19
19
  from hockey_blast_common_lib.progress_utils import create_progress_tracker
20
20
 
21
+ # Import status constants for game filtering
22
+ FINAL_STATUS = "Final"
23
+ FINAL_SO_STATUS = "Final(SO)"
24
+ FORFEIT_STATUS = "FORFEIT"
25
+ NOEVENTS_STATUS = "NOEVENTS"
26
+
21
27
  def calculate_current_point_streak(session, human_id, filter_condition):
22
28
  """
23
29
  Calculate the current point streak for a player.
@@ -138,16 +144,26 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, debug_huma
138
144
  # human_filter = [GameRoster.human_id == debug_human_id]
139
145
 
140
146
  # Aggregate games played for each human in each division, excluding goalies
147
+ # games_participated: Count FINAL, FINAL_SO, FORFEIT, NOEVENTS
148
+ # games_with_stats: Count only FINAL, FINAL_SO (for per-game averages)
141
149
  games_played_query = session.query(
142
150
  GameRoster.human_id,
143
- func.count(Game.id).label('games_played'),
151
+ func.count(Game.id).label('games_played'), # DEPRECATED - will be replaced by games_participated
152
+ func.sum(case(
153
+ (Game.status.in_([FINAL_STATUS, FINAL_SO_STATUS, FORFEIT_STATUS, NOEVENTS_STATUS]), 1),
154
+ else_=0
155
+ )).label('games_participated'),
156
+ func.sum(case(
157
+ (Game.status.in_([FINAL_STATUS, FINAL_SO_STATUS]), 1),
158
+ else_=0
159
+ )).label('games_with_stats'),
144
160
  func.array_agg(Game.id).label('game_ids')
145
161
  ).join(Game, Game.id == GameRoster.game_id)
146
-
162
+
147
163
  # Only join Division if not level aggregation (since we filter on Game.division_id directly for levels)
148
164
  if aggregation_type != 'level':
149
165
  games_played_query = games_played_query.join(Division, Game.division_id == Division.id)
150
-
166
+
151
167
  games_played_stats = games_played_query.filter(filter_condition, ~GameRoster.role.ilike('g'), *human_filter).group_by(GameRoster.human_id).all()
152
168
 
153
169
  # Aggregate goals for each human in each division, excluding goalies
@@ -206,7 +222,9 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, debug_huma
206
222
  key = (aggregation_id, stat.human_id)
207
223
  if key not in stats_dict:
208
224
  stats_dict[key] = {
209
- 'games_played': 0,
225
+ 'games_played': 0, # DEPRECATED - for backward compatibility
226
+ 'games_participated': 0, # Total games: FINAL, FINAL_SO, FORFEIT, NOEVENTS
227
+ 'games_with_stats': 0, # Games with full stats: FINAL, FINAL_SO only
210
228
  'goals': 0,
211
229
  'assists': 0,
212
230
  'penalties': 0,
@@ -224,6 +242,8 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, debug_huma
224
242
  'last_game_id': None
225
243
  }
226
244
  stats_dict[key]['games_played'] += stat.games_played
245
+ stats_dict[key]['games_participated'] += stat.games_participated
246
+ stats_dict[key]['games_with_stats'] += stat.games_with_stats
227
247
  stats_dict[key]['game_ids'].extend(stat.game_ids)
228
248
 
229
249
  # Filter out entries with games_played less than min_games
@@ -253,14 +273,14 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, debug_huma
253
273
  stats_dict[key]['penalties'] += stat.penalties
254
274
  stats_dict[key]['gm_penalties'] += stat.gm_penalties # Update GM penalties
255
275
 
256
- # Calculate per game stats
276
+ # Calculate per game stats (using games_with_stats as denominator for accuracy)
257
277
  for key, stat in stats_dict.items():
258
- if stat['games_played'] > 0:
259
- stat['goals_per_game'] = stat['goals'] / stat['games_played']
260
- stat['points_per_game'] = stat['points'] / stat['games_played']
261
- stat['assists_per_game'] = stat['assists'] / stat['games_played']
262
- stat['penalties_per_game'] = stat['penalties'] / stat['games_played']
263
- stat['gm_penalties_per_game'] = stat['gm_penalties'] / stat['games_played'] # Calculate GM penalties per game
278
+ if stat['games_with_stats'] > 0:
279
+ stat['goals_per_game'] = stat['goals'] / stat['games_with_stats']
280
+ stat['points_per_game'] = stat['points'] / stat['games_with_stats']
281
+ stat['assists_per_game'] = stat['assists'] / stat['games_with_stats']
282
+ stat['penalties_per_game'] = stat['penalties'] / stat['games_with_stats']
283
+ stat['gm_penalties_per_game'] = stat['gm_penalties'] / stat['games_with_stats'] # Calculate GM penalties per game
264
284
 
265
285
  # Ensure all keys have valid human_id values
266
286
  stats_dict = {key: value for key, value in stats_dict.items() if key[1] is not None}
@@ -292,6 +312,8 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, debug_huma
292
312
  stats_dict[key][f'{field}_rank'] = rank
293
313
 
294
314
  assign_ranks(stats_dict, 'games_played')
315
+ assign_ranks(stats_dict, 'games_participated') # Rank by total participation
316
+ assign_ranks(stats_dict, 'games_with_stats') # Rank by games with full stats
295
317
  assign_ranks(stats_dict, 'goals')
296
318
  assign_ranks(stats_dict, 'assists')
297
319
  assign_ranks(stats_dict, 'points')
@@ -330,7 +352,11 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, debug_huma
330
352
  skater_stat = StatsModel(
331
353
  aggregation_id=aggregation_id,
332
354
  human_id=human_id,
333
- games_played=stat['games_played'],
355
+ games_played=stat['games_played'], # DEPRECATED - for backward compatibility
356
+ games_participated=stat['games_participated'], # Total games: FINAL, FINAL_SO, FORFEIT, NOEVENTS
357
+ games_participated_rank=stat['games_participated_rank'],
358
+ games_with_stats=stat['games_with_stats'], # Games with full stats: FINAL, FINAL_SO only
359
+ games_with_stats_rank=stat['games_with_stats_rank'],
334
360
  goals=stat['goals'],
335
361
  assists=stat['assists'],
336
362
  points=stat['goals'] + stat['assists'],
@@ -4,26 +4,34 @@ from datetime import datetime, timedelta
4
4
  class ProgressTracker:
5
5
  """
6
6
  Reusable progress tracker with ETA calculation for stats aggregation processes.
7
+ Supports optional custom counters for tracking additional metrics.
7
8
  """
8
-
9
- def __init__(self, total_items, description="Processing"):
9
+
10
+ def __init__(self, total_items, description="Processing", custom_counters=None):
10
11
  self.total_items = total_items
11
12
  self.description = description
12
13
  self.start_time = time.time()
13
14
  self.processed_items = 0
14
15
  self.last_update_time = self.start_time
16
+ self.custom_counters = custom_counters if custom_counters else {}
15
17
 
16
- def update(self, processed_count=None):
18
+ def update(self, processed_count=None, **kwargs):
17
19
  """
18
20
  Update progress. If processed_count is None, increment by 1.
21
+ kwargs can be used to update custom counters.
19
22
  """
20
23
  if processed_count is not None:
21
24
  self.processed_items = processed_count
22
25
  else:
23
26
  self.processed_items += 1
24
-
27
+
28
+ # Update custom counters
29
+ for key, value in kwargs.items():
30
+ if key in self.custom_counters:
31
+ self.custom_counters[key] = value
32
+
25
33
  current_time = time.time()
26
-
34
+
27
35
  # Only update display if at least 0.1 seconds have passed (to avoid spamming)
28
36
  if current_time - self.last_update_time >= 0.1 or self.processed_items == self.total_items:
29
37
  self._display_progress()
@@ -52,10 +60,15 @@ class ProgressTracker:
52
60
  eta_str = self._format_time(eta_seconds)
53
61
 
54
62
  elapsed_str = self._format_time(elapsed_time)
55
-
63
+
56
64
  progress_msg = f"\r{self.description}: {self.processed_items}/{self.total_items} ({percentage:.1f}%) | "
57
65
  progress_msg += f"Elapsed: {elapsed_str} | ETA: {eta_str}"
58
-
66
+
67
+ # Add custom counters to the display
68
+ if self.custom_counters:
69
+ counter_parts = [f"{key}: {value}" for key, value in self.custom_counters.items()]
70
+ progress_msg += " | " + ", ".join(counter_parts)
71
+
59
72
  print(progress_msg, end="", flush=True)
60
73
 
61
74
  # Add newline when complete
@@ -84,8 +97,9 @@ class ProgressTracker:
84
97
  self.processed_items = self.total_items
85
98
  self._display_progress()
86
99
 
87
- def create_progress_tracker(total_items, description="Processing"):
100
+ def create_progress_tracker(total_items, description="Processing", custom_counters=None):
88
101
  """
89
102
  Factory function to create a progress tracker.
103
+ custom_counters: dict of counter_name: initial_value for additional metrics to track
90
104
  """
91
- return ProgressTracker(total_items, description)
105
+ return ProgressTracker(total_items, description, custom_counters)
@@ -52,8 +52,12 @@ class BaseStatsSkater(db.Model):
52
52
  __abstract__ = True
53
53
  id = db.Column(db.Integer, primary_key=True)
54
54
  human_id = db.Column(db.Integer, db.ForeignKey('humans.id'), nullable=False)
55
- games_played = db.Column(db.Integer, default=0)
55
+ games_played = db.Column(db.Integer, default=0) # DEPRECATED - use games_participated instead
56
56
  games_played_rank = db.Column(db.Integer, default=0)
57
+ games_participated = db.Column(db.Integer, default=0) # Count FINAL, FINAL_SO, FORFEIT, NOEVENTS
58
+ games_participated_rank = db.Column(db.Integer, default=0)
59
+ games_with_stats = db.Column(db.Integer, default=0) # Count only FINAL, FINAL_SO
60
+ games_with_stats_rank = db.Column(db.Integer, default=0)
57
61
  goals = db.Column(db.Integer, default=0)
58
62
  goals_rank = db.Column(db.Integer, default=0)
59
63
  assists = db.Column(db.Integer, default=0)
@@ -104,8 +108,12 @@ class BaseStatsGoalie(db.Model):
104
108
  __abstract__ = True
105
109
  id = db.Column(db.Integer, primary_key=True)
106
110
  human_id = db.Column(db.Integer, db.ForeignKey('humans.id'), nullable=False)
107
- games_played = db.Column(db.Integer, default=0)
111
+ games_played = db.Column(db.Integer, default=0) # DEPRECATED - use games_participated instead
108
112
  games_played_rank = db.Column(db.Integer, default=0)
113
+ games_participated = db.Column(db.Integer, default=0) # Count FINAL, FINAL_SO, FORFEIT, NOEVENTS
114
+ games_participated_rank = db.Column(db.Integer, default=0)
115
+ games_with_stats = db.Column(db.Integer, default=0) # Count only FINAL, FINAL_SO
116
+ games_with_stats_rank = db.Column(db.Integer, default=0)
109
117
  goals_allowed = db.Column(db.Integer, default=0)
110
118
  goals_allowed_rank = db.Column(db.Integer, default=0)
111
119
  goals_allowed_per_game = db.Column(db.Float, default=0.0)
@@ -137,8 +145,12 @@ class BaseStatsReferee(db.Model):
137
145
  __abstract__ = True
138
146
  id = db.Column(db.Integer, primary_key=True)
139
147
  human_id = db.Column(db.Integer, db.ForeignKey('humans.id'), nullable=False)
140
- games_reffed = db.Column(db.Integer, default=0)
148
+ games_reffed = db.Column(db.Integer, default=0) # DEPRECATED - use games_participated instead
141
149
  games_reffed_rank = db.Column(db.Integer, default=0)
150
+ games_participated = db.Column(db.Integer, default=0) # Count FINAL, FINAL_SO, FORFEIT, NOEVENTS
151
+ games_participated_rank = db.Column(db.Integer, default=0)
152
+ games_with_stats = db.Column(db.Integer, default=0) # Count only FINAL, FINAL_SO (for per-game averages)
153
+ games_with_stats_rank = db.Column(db.Integer, default=0)
142
154
  penalties_given = db.Column(db.Integer, default=0)
143
155
  penalties_given_rank = db.Column(db.Integer, default=0)
144
156
  penalties_per_game = db.Column(db.Float, default=0.0)
@@ -170,8 +182,12 @@ class BaseStatsScorekeeper(db.Model):
170
182
  __abstract__ = True
171
183
  id = db.Column(db.Integer, primary_key=True)
172
184
  human_id = db.Column(db.Integer, db.ForeignKey('humans.id'), nullable=False)
173
- games_recorded = db.Column(db.Integer, default=0)
185
+ games_recorded = db.Column(db.Integer, default=0) # DEPRECATED - use games_participated instead
174
186
  games_recorded_rank = db.Column(db.Integer, default=0)
187
+ games_participated = db.Column(db.Integer, default=0) # Count FINAL, FINAL_SO, FORFEIT, NOEVENTS
188
+ games_participated_rank = db.Column(db.Integer, default=0)
189
+ games_with_stats = db.Column(db.Integer, default=0) # Count only FINAL, FINAL_SO (for per-game averages)
190
+ games_with_stats_rank = db.Column(db.Integer, default=0)
175
191
  sog_given = db.Column(db.Integer, default=0)
176
192
  sog_given_rank = db.Column(db.Integer, default=0)
177
193
  sog_per_game = db.Column(db.Float, default=0.0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hockey-blast-common-lib
3
- Version: 0.1.61
3
+ Version: 0.1.62
4
4
  Summary: Common library for shared functionality and DB models
5
5
  Author: Pavel Kletskov
6
6
  Author-email: kletskov@gmail.com
@@ -1,28 +1,28 @@
1
1
  hockey_blast_common_lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  hockey_blast_common_lib/aggregate_all_stats.py,sha256=QhuSvGjuk4jVywNRcgxB-9ooJAoAbZRkaLjLe9Q1hEM,1363
3
- hockey_blast_common_lib/aggregate_goalie_stats.py,sha256=WmKqGdM0SZLYXailCsBM4Wdpy8kOcmPtF9JOxLxzmRc,13217
3
+ hockey_blast_common_lib/aggregate_goalie_stats.py,sha256=5FwmRAW8ydgFd7LGZtH1j-7TCA50piT6igCsuy8bBqo,14883
4
4
  hockey_blast_common_lib/aggregate_h2h_stats.py,sha256=dC5TcJZGkpIQTiq3z40kOX6EjEhFbGv5EL0P1EClBQ0,11117
5
5
  hockey_blast_common_lib/aggregate_human_stats.py,sha256=JZcXJofF1wmFPcLbalM_GiXrq22eUJO28R_xyypw4rI,25456
6
- hockey_blast_common_lib/aggregate_referee_stats.py,sha256=oX9UgD_mvvDJv4Sy1jJemUKWNi3N-1sOjfHNSg4VzOE,13351
6
+ hockey_blast_common_lib/aggregate_referee_stats.py,sha256=rVxbuDjxYbI5zNlvOSbSHeG8yeuDpJSLNmjZKn3rBhQ,15764
7
7
  hockey_blast_common_lib/aggregate_s2s_stats.py,sha256=urYN0Q06twwLO-XWGlSMVAVOTVR_D2AWdmoGsxIYHXE,6737
8
- hockey_blast_common_lib/aggregate_scorekeeper_stats.py,sha256=_SWt7qqUl31As1pkEJ40YLh9ItXdBuen3RjtSzHGP8U,11997
9
- hockey_blast_common_lib/aggregate_skater_stats.py,sha256=Y3zDeyEWuEae-PdnFMz7aGRzP4BnYNTut_zqRgR56yI,22715
8
+ hockey_blast_common_lib/aggregate_scorekeeper_stats.py,sha256=_0HR-KsuEPM7LFTVvmm8RwH_Y1_H9qrvW-wEMFI7LNU,13488
9
+ hockey_blast_common_lib/aggregate_skater_stats.py,sha256=Tp7tJ03YdlVsm5yBNJYc7TC243m7GRtcHWO2o4X6kG4,24383
10
10
  hockey_blast_common_lib/assign_skater_skill.py,sha256=Asq6iRMPsCMDnvuNSd-M3s4Gee4kDocP9Eznwju_9kA,2749
11
11
  hockey_blast_common_lib/db_connection.py,sha256=HvPxDvOj7j5H85RfslGvHVNevfg7mKCd0syJ6NX21mU,1890
12
12
  hockey_blast_common_lib/dump_sample_db.sh,sha256=MY3lnzTXBoWd76-ZlZr9nWsKMEVgyRsUn-LZ2d1JWZs,810
13
13
  hockey_blast_common_lib/h2h_models.py,sha256=0st4xoJO0U6ONfx3BV03BQvHjZE31e_PqZfphAJMoSU,7968
14
- hockey_blast_common_lib/hockey_blast_sample_backup.sql.gz,sha256=vJK83rusOv9VoljFTC03-0leXhGBHa1WV0XvM2ri870,4648906
14
+ hockey_blast_common_lib/hockey_blast_sample_backup.sql.gz,sha256=57W5sPGpAp01YtIe5XrQeCgBCDFTiI-347nuzN3d3IU,4648906
15
15
  hockey_blast_common_lib/models.py,sha256=ccM886RcSFDiJ3yj2l9OqRi_PwR1L6WU8AsqzgV3_t0,19598
16
16
  hockey_blast_common_lib/options.py,sha256=XecGGlizbul7BnMePrvIqvEq5_w49UoG3Yu9iv961gg,1499
17
- hockey_blast_common_lib/progress_utils.py,sha256=H_zRFOsb2qQQpGw56wJghZ1nUe_m6zqGeR9hZ33Y1Uo,3229
17
+ hockey_blast_common_lib/progress_utils.py,sha256=7cqyUTMmW3xAIh5JKFlhnBiybCJ9WvGDz7ihH59Lc_0,3953
18
18
  hockey_blast_common_lib/restore_sample_db.sh,sha256=7W3lzRZeu9zXIu1Bvtnaw8EHc1ulHmFM4mMh86oUQJo,2205
19
19
  hockey_blast_common_lib/skills_in_divisions.py,sha256=m-UEwMwn1KM7wOYvDstgsOEeH57M9V6yrkBoghzGYKE,7005
20
20
  hockey_blast_common_lib/skills_propagation.py,sha256=nUxntyK8M4xWjHpkfze8f0suaBeunxicgDCduGmNJ-A,18468
21
- hockey_blast_common_lib/stats_models.py,sha256=64sUq_iWhNXi_b_V_1INuQ1RusKaTASjurkRo5gQOs4,26703
21
+ hockey_blast_common_lib/stats_models.py,sha256=yLfsR0RhSc95-8ULdJ8tuvLd6RjIFkgZ74ejubJYVUw,28187
22
22
  hockey_blast_common_lib/stats_utils.py,sha256=DXsPO4jw8XsdRUN46TGF_IiBAfz3GCIVBswCGp5ELDk,284
23
23
  hockey_blast_common_lib/utils.py,sha256=1YJRAj1lhftjIAM2frFi4A4K90kCJaxWlgBQ1-77xZY,6486
24
24
  hockey_blast_common_lib/wsgi.py,sha256=y3NxoJfWjdzX3iP7RGvDEer6zcnPyCanpqSgW1BlXgg,779
25
- hockey_blast_common_lib-0.1.61.dist-info/METADATA,sha256=AtOG9FeESKu9wi75z2Q9cOctyOnmyoNYfu6hkiCFn0c,318
26
- hockey_blast_common_lib-0.1.61.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
27
- hockey_blast_common_lib-0.1.61.dist-info/top_level.txt,sha256=wIR4LIkE40npoA2QlOdfCYlgFeGbsHR8Z6r0h46Vtgc,24
28
- hockey_blast_common_lib-0.1.61.dist-info/RECORD,,
25
+ hockey_blast_common_lib-0.1.62.dist-info/METADATA,sha256=zCGOcewa8kzP4jNrlDR-xmPFh7SygSOsYtXXG9kVhRc,318
26
+ hockey_blast_common_lib-0.1.62.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
27
+ hockey_blast_common_lib-0.1.62.dist-info/top_level.txt,sha256=wIR4LIkE40npoA2QlOdfCYlgFeGbsHR8Z6r0h46Vtgc,24
28
+ hockey_blast_common_lib-0.1.62.dist-info/RECORD,,