hockey-blast-common-lib 0.1.65__py3-none-any.whl → 0.1.67__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_all_stats.py +14 -0
- hockey_blast_common_lib/aggregate_game_stats_all.py +150 -0
- hockey_blast_common_lib/aggregate_game_stats_goalie.py +257 -0
- hockey_blast_common_lib/aggregate_game_stats_skater.py +361 -0
- hockey_blast_common_lib/aggregate_goalie_stats.py +79 -0
- hockey_blast_common_lib/aggregate_human_stats.py +12 -0
- hockey_blast_common_lib/aggregate_referee_stats.py +75 -0
- hockey_blast_common_lib/aggregate_scorekeeper_stats.py +91 -0
- hockey_blast_common_lib/aggregate_skater_stats.py +118 -0
- hockey_blast_common_lib/aggregate_team_goalie_stats.py +265 -0
- hockey_blast_common_lib/aggregate_team_skater_stats.py +313 -0
- hockey_blast_common_lib/hockey_blast_sample_backup.sql.gz +0 -0
- hockey_blast_common_lib/models.py +16 -1
- hockey_blast_common_lib/stats_models.py +211 -5
- hockey_blast_common_lib/utils.py +65 -0
- {hockey_blast_common_lib-0.1.65.dist-info → hockey_blast_common_lib-0.1.67.dist-info}/METADATA +1 -1
- hockey_blast_common_lib-0.1.67.dist-info/RECORD +34 -0
- hockey_blast_common_lib-0.1.65.dist-info/RECORD +0 -29
- {hockey_blast_common_lib-0.1.65.dist-info → hockey_blast_common_lib-0.1.67.dist-info}/WHEEL +0 -0
- {hockey_blast_common_lib-0.1.65.dist-info → hockey_blast_common_lib-0.1.67.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
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 datetime import datetime, timedelta
|
|
8
|
+
|
|
9
|
+
from sqlalchemy import and_, case, func
|
|
10
|
+
from sqlalchemy.exc import IntegrityError
|
|
11
|
+
|
|
12
|
+
from hockey_blast_common_lib.db_connection import create_session
|
|
13
|
+
from hockey_blast_common_lib.models import Division, Game, GameRoster, Goal, Human, Penalty
|
|
14
|
+
from hockey_blast_common_lib.progress_utils import create_progress_tracker
|
|
15
|
+
from hockey_blast_common_lib.stats_models import GameStatsSkater
|
|
16
|
+
from hockey_blast_common_lib.utils import get_non_human_ids
|
|
17
|
+
|
|
18
|
+
# Import status constants for game filtering
|
|
19
|
+
FINAL_STATUS = "Final"
|
|
20
|
+
FINAL_SO_STATUS = "Final(SO)"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def aggregate_game_stats_skater(session, mode="full", human_id=None):
|
|
24
|
+
"""Aggregate per-game skater statistics.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
session: Database session
|
|
28
|
+
mode: "full" to regenerate all records, "append" to process new games only
|
|
29
|
+
human_id: Optional human_id to process only one player (for testing/debugging)
|
|
30
|
+
|
|
31
|
+
The function stores individual game performance for each skater with non-zero stats.
|
|
32
|
+
Only games where the player recorded at least one goal, assist, or penalty minute are saved.
|
|
33
|
+
This sparse storage is optimized for RAG system queries.
|
|
34
|
+
|
|
35
|
+
Uses Incognito Human sentinel record (game_id=-1) to track last processed timestamp
|
|
36
|
+
for append mode with 1-day overlap to catch data corrections.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# Get Incognito Human for sentinel tracking (first_name="Incognito", middle_name="", last_name="Human")
|
|
40
|
+
incognito_human = session.query(Human).filter_by(
|
|
41
|
+
first_name="Incognito", middle_name="", last_name="Human"
|
|
42
|
+
).first()
|
|
43
|
+
if not incognito_human:
|
|
44
|
+
raise RuntimeError("Incognito Human not found in database - required for sentinel tracking")
|
|
45
|
+
incognito_human_id = incognito_human.id
|
|
46
|
+
|
|
47
|
+
non_human_ids = get_non_human_ids(session)
|
|
48
|
+
|
|
49
|
+
# Add human_id to filter if specified
|
|
50
|
+
if human_id:
|
|
51
|
+
human = session.query(Human).filter_by(id=human_id).first()
|
|
52
|
+
if not human:
|
|
53
|
+
print(f"ERROR: Human ID {human_id} not found in database")
|
|
54
|
+
return
|
|
55
|
+
print(f"Limiting to human_id={human_id}: {human.first_name} {human.last_name}\n")
|
|
56
|
+
|
|
57
|
+
print(f"\n{'='*80}")
|
|
58
|
+
print(f"Aggregating per-game skater statistics (mode: {mode})")
|
|
59
|
+
print(f"{'='*80}\n")
|
|
60
|
+
|
|
61
|
+
# Determine game filtering based on mode
|
|
62
|
+
if mode == "append":
|
|
63
|
+
# Query sentinel record for last processed timestamp
|
|
64
|
+
sentinel = (
|
|
65
|
+
session.query(GameStatsSkater)
|
|
66
|
+
.filter(
|
|
67
|
+
GameStatsSkater.human_id == incognito_human_id,
|
|
68
|
+
GameStatsSkater.game_id == -1,
|
|
69
|
+
)
|
|
70
|
+
.first()
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if sentinel:
|
|
74
|
+
last_processed = datetime.combine(sentinel.game_date, sentinel.game_time)
|
|
75
|
+
# Subtract 1 day for overlap to catch data corrections
|
|
76
|
+
start_datetime = last_processed - timedelta(days=1)
|
|
77
|
+
print(f"Append mode: Processing games after {start_datetime}")
|
|
78
|
+
print(f"(1-day overlap from last processed: {last_processed})\n")
|
|
79
|
+
|
|
80
|
+
# Delete records for games in the overlap window
|
|
81
|
+
delete_count = (
|
|
82
|
+
session.query(GameStatsSkater)
|
|
83
|
+
.filter(
|
|
84
|
+
GameStatsSkater.human_id != incognito_human_id,
|
|
85
|
+
func.cast(
|
|
86
|
+
func.concat(GameStatsSkater.game_date, " ", GameStatsSkater.game_time),
|
|
87
|
+
func.TIMESTAMP,
|
|
88
|
+
) >= start_datetime,
|
|
89
|
+
)
|
|
90
|
+
.delete(synchronize_session=False)
|
|
91
|
+
)
|
|
92
|
+
session.commit()
|
|
93
|
+
print(f"Deleted {delete_count} existing records in overlap window\n")
|
|
94
|
+
else:
|
|
95
|
+
# No sentinel found, treat as full mode
|
|
96
|
+
print("No sentinel record found - treating as full mode\n")
|
|
97
|
+
mode = "full"
|
|
98
|
+
start_datetime = None
|
|
99
|
+
else:
|
|
100
|
+
start_datetime = None
|
|
101
|
+
|
|
102
|
+
if mode == "full":
|
|
103
|
+
# Delete all existing records except sentinel
|
|
104
|
+
delete_count = (
|
|
105
|
+
session.query(GameStatsSkater)
|
|
106
|
+
.filter(GameStatsSkater.human_id != incognito_human_id)
|
|
107
|
+
.delete(synchronize_session=False)
|
|
108
|
+
)
|
|
109
|
+
session.commit()
|
|
110
|
+
print(f"Full mode: Deleted {delete_count} existing records\n")
|
|
111
|
+
|
|
112
|
+
# Build game filter for eligible games
|
|
113
|
+
game_filter = Game.status.in_([FINAL_STATUS, FINAL_SO_STATUS])
|
|
114
|
+
if mode == "append" and start_datetime:
|
|
115
|
+
game_filter = and_(
|
|
116
|
+
game_filter,
|
|
117
|
+
func.cast(
|
|
118
|
+
func.concat(Game.date, " ", Game.time),
|
|
119
|
+
func.TIMESTAMP,
|
|
120
|
+
) >= start_datetime,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Count total games to process for progress tracking
|
|
124
|
+
total_games = session.query(Game).filter(game_filter).count()
|
|
125
|
+
print(f"Processing {total_games} games...\n")
|
|
126
|
+
|
|
127
|
+
if total_games == 0:
|
|
128
|
+
print("No games to process.\n")
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
# Query game roster entries for skaters (exclude goalies)
|
|
132
|
+
# Join with games to get metadata, filter by game status and date window
|
|
133
|
+
roster_query = (
|
|
134
|
+
session.query(
|
|
135
|
+
GameRoster.game_id,
|
|
136
|
+
GameRoster.human_id,
|
|
137
|
+
GameRoster.team_id,
|
|
138
|
+
Game.org_id,
|
|
139
|
+
Division.level_id,
|
|
140
|
+
Game.date.label("game_date"),
|
|
141
|
+
Game.time.label("game_time"),
|
|
142
|
+
)
|
|
143
|
+
.join(Game, GameRoster.game_id == Game.id)
|
|
144
|
+
.join(Division, Game.division_id == Division.id)
|
|
145
|
+
.filter(
|
|
146
|
+
~GameRoster.role.ilike("g"), # Exclude goalies
|
|
147
|
+
GameRoster.human_id.notin_(non_human_ids), # Filter placeholder humans
|
|
148
|
+
game_filter,
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Add human_id filter if specified
|
|
153
|
+
if human_id:
|
|
154
|
+
roster_query = roster_query.filter(GameRoster.human_id == human_id)
|
|
155
|
+
|
|
156
|
+
roster_entries = roster_query.all()
|
|
157
|
+
|
|
158
|
+
# Build dict of roster entries by (game_id, human_id) for fast lookup
|
|
159
|
+
roster_dict = {}
|
|
160
|
+
for entry in roster_entries:
|
|
161
|
+
key = (entry.game_id, entry.human_id)
|
|
162
|
+
roster_dict[key] = {
|
|
163
|
+
"team_id": entry.team_id,
|
|
164
|
+
"org_id": entry.org_id,
|
|
165
|
+
"level_id": entry.level_id,
|
|
166
|
+
"game_date": entry.game_date,
|
|
167
|
+
"game_time": entry.game_time,
|
|
168
|
+
"goals": 0,
|
|
169
|
+
"assists": 0,
|
|
170
|
+
"points": 0,
|
|
171
|
+
"penalty_minutes": 0,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
print(f"Found {len(roster_dict)} skater roster entries\n")
|
|
175
|
+
|
|
176
|
+
# Query goals and count by scorer and assisters
|
|
177
|
+
print("Aggregating goals and assists...")
|
|
178
|
+
goals = (
|
|
179
|
+
session.query(Goal)
|
|
180
|
+
.join(Game, Goal.game_id == Game.id)
|
|
181
|
+
.filter(game_filter)
|
|
182
|
+
.all()
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
for goal in goals:
|
|
186
|
+
# Count goal for scorer
|
|
187
|
+
key = (goal.game_id, goal.goal_scorer_id)
|
|
188
|
+
if key in roster_dict:
|
|
189
|
+
roster_dict[key]["goals"] += 1
|
|
190
|
+
roster_dict[key]["points"] += 1
|
|
191
|
+
|
|
192
|
+
# Count assists
|
|
193
|
+
if goal.assist_1_id:
|
|
194
|
+
key = (goal.game_id, goal.assist_1_id)
|
|
195
|
+
if key in roster_dict:
|
|
196
|
+
roster_dict[key]["assists"] += 1
|
|
197
|
+
roster_dict[key]["points"] += 1
|
|
198
|
+
|
|
199
|
+
if goal.assist_2_id:
|
|
200
|
+
key = (goal.game_id, goal.assist_2_id)
|
|
201
|
+
if key in roster_dict:
|
|
202
|
+
roster_dict[key]["assists"] += 1
|
|
203
|
+
roster_dict[key]["points"] += 1
|
|
204
|
+
|
|
205
|
+
print(f"Processed {len(goals)} goals\n")
|
|
206
|
+
|
|
207
|
+
# Query penalties and aggregate by penalized player
|
|
208
|
+
print("Aggregating penalties...")
|
|
209
|
+
penalties = (
|
|
210
|
+
session.query(Penalty)
|
|
211
|
+
.join(Game, Penalty.game_id == Game.id)
|
|
212
|
+
.filter(game_filter)
|
|
213
|
+
.all()
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
for penalty in penalties:
|
|
217
|
+
key = (penalty.game_id, penalty.penalized_player_id)
|
|
218
|
+
if key in roster_dict:
|
|
219
|
+
# Convert penalty minutes: "GM" (game misconduct) = 10, else parse integer
|
|
220
|
+
if penalty.penalty_minutes and penalty.penalty_minutes.upper() == "GM":
|
|
221
|
+
roster_dict[key]["penalty_minutes"] += 10
|
|
222
|
+
else:
|
|
223
|
+
try:
|
|
224
|
+
minutes = int(penalty.penalty_minutes) if penalty.penalty_minutes else 0
|
|
225
|
+
roster_dict[key]["penalty_minutes"] += minutes
|
|
226
|
+
except (ValueError, TypeError):
|
|
227
|
+
# Log unconvertible values but don't crash
|
|
228
|
+
print(f"Warning: Could not convert penalty_minutes '{penalty.penalty_minutes}' for penalty {penalty.id}")
|
|
229
|
+
|
|
230
|
+
print(f"Processed {len(penalties)} penalties\n")
|
|
231
|
+
|
|
232
|
+
# Filter to only non-zero stats (CRITICAL for RAG efficiency)
|
|
233
|
+
print("Filtering to non-zero records...")
|
|
234
|
+
nonzero_dict = {
|
|
235
|
+
key: stats
|
|
236
|
+
for key, stats in roster_dict.items()
|
|
237
|
+
if stats["goals"] > 0 or stats["assists"] > 0 or stats["penalty_minutes"] > 0
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
print(f"Filtered: {len(nonzero_dict)} non-zero records (from {len(roster_dict)} total)\n")
|
|
241
|
+
|
|
242
|
+
# Insert records in batches with progress tracking
|
|
243
|
+
batch_size = 1000
|
|
244
|
+
total_records = len(nonzero_dict)
|
|
245
|
+
|
|
246
|
+
if total_records == 0:
|
|
247
|
+
print("No non-zero records to insert.\n")
|
|
248
|
+
else:
|
|
249
|
+
progress = create_progress_tracker(total_records, "Inserting per-game skater stats")
|
|
250
|
+
|
|
251
|
+
records_to_insert = []
|
|
252
|
+
for i, (key, stats) in enumerate(nonzero_dict.items(), 1):
|
|
253
|
+
game_id, human_id = key
|
|
254
|
+
|
|
255
|
+
record = GameStatsSkater(
|
|
256
|
+
game_id=game_id,
|
|
257
|
+
human_id=human_id,
|
|
258
|
+
team_id=stats["team_id"],
|
|
259
|
+
org_id=stats["org_id"],
|
|
260
|
+
level_id=stats["level_id"],
|
|
261
|
+
game_date=stats["game_date"],
|
|
262
|
+
game_time=stats["game_time"],
|
|
263
|
+
goals=stats["goals"],
|
|
264
|
+
assists=stats["assists"],
|
|
265
|
+
points=stats["points"],
|
|
266
|
+
penalty_minutes=stats["penalty_minutes"],
|
|
267
|
+
created_at=datetime.utcnow(),
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
records_to_insert.append(record)
|
|
271
|
+
|
|
272
|
+
# Commit in batches
|
|
273
|
+
if i % batch_size == 0 or i == total_records:
|
|
274
|
+
session.bulk_save_objects(records_to_insert)
|
|
275
|
+
session.commit()
|
|
276
|
+
records_to_insert = []
|
|
277
|
+
progress.update(i)
|
|
278
|
+
|
|
279
|
+
print("\nInsert complete.\n")
|
|
280
|
+
|
|
281
|
+
# Update or create sentinel record with max game timestamp (skip if filtering by human_id)
|
|
282
|
+
if not human_id:
|
|
283
|
+
max_game = (
|
|
284
|
+
session.query(
|
|
285
|
+
Game.date.label("game_date"),
|
|
286
|
+
Game.time.label("game_time"),
|
|
287
|
+
)
|
|
288
|
+
.filter(game_filter)
|
|
289
|
+
.order_by(Game.date.desc(), Game.time.desc())
|
|
290
|
+
.first()
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
if max_game:
|
|
294
|
+
# Try to update existing sentinel
|
|
295
|
+
sentinel = (
|
|
296
|
+
session.query(GameStatsSkater)
|
|
297
|
+
.filter(
|
|
298
|
+
GameStatsSkater.human_id == incognito_human_id,
|
|
299
|
+
GameStatsSkater.game_id == -1,
|
|
300
|
+
)
|
|
301
|
+
.first()
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
if sentinel:
|
|
305
|
+
sentinel.game_date = max_game.game_date
|
|
306
|
+
sentinel.game_time = max_game.game_time
|
|
307
|
+
print(f"Updated sentinel record: {max_game.game_date} {max_game.game_time}")
|
|
308
|
+
else:
|
|
309
|
+
# Create new sentinel
|
|
310
|
+
sentinel = GameStatsSkater(
|
|
311
|
+
game_id=-1,
|
|
312
|
+
human_id=incognito_human_id,
|
|
313
|
+
team_id=-1, # Dummy value
|
|
314
|
+
org_id=-1, # Dummy value
|
|
315
|
+
level_id=-1, # Dummy value
|
|
316
|
+
game_date=max_game.game_date,
|
|
317
|
+
game_time=max_game.game_time,
|
|
318
|
+
goals=0,
|
|
319
|
+
assists=0,
|
|
320
|
+
points=0,
|
|
321
|
+
penalty_minutes=0,
|
|
322
|
+
created_at=datetime.utcnow(),
|
|
323
|
+
)
|
|
324
|
+
session.add(sentinel)
|
|
325
|
+
print(f"Created sentinel record: {max_game.game_date} {max_game.game_time}")
|
|
326
|
+
|
|
327
|
+
session.commit()
|
|
328
|
+
else:
|
|
329
|
+
print("Skipping sentinel record creation (human_id filter active)")
|
|
330
|
+
|
|
331
|
+
print(f"\n{'='*80}")
|
|
332
|
+
print("Per-game skater statistics aggregation complete")
|
|
333
|
+
print(f"{'='*80}\n")
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def run_aggregate_game_stats_skater():
|
|
337
|
+
"""Main entry point for skater per-game aggregation."""
|
|
338
|
+
import argparse
|
|
339
|
+
|
|
340
|
+
parser = argparse.ArgumentParser(description="Aggregate per-game skater statistics")
|
|
341
|
+
parser.add_argument(
|
|
342
|
+
"--mode",
|
|
343
|
+
choices=["full", "append"],
|
|
344
|
+
default="full",
|
|
345
|
+
help="Aggregation mode: 'full' to regenerate all, 'append' to add new games only",
|
|
346
|
+
)
|
|
347
|
+
parser.add_argument(
|
|
348
|
+
"--human-id",
|
|
349
|
+
type=int,
|
|
350
|
+
default=None,
|
|
351
|
+
help="Optional: Limit processing to specific human_id (for testing)",
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
args = parser.parse_args()
|
|
355
|
+
|
|
356
|
+
session = create_session("boss")
|
|
357
|
+
aggregate_game_stats_skater(session, mode=args.mode, human_id=args.human_id)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
if __name__ == "__main__":
|
|
361
|
+
run_aggregate_game_stats_skater()
|
|
@@ -35,8 +35,10 @@ from hockey_blast_common_lib.stats_models import (
|
|
|
35
35
|
from hockey_blast_common_lib.stats_utils import ALL_ORGS_ID
|
|
36
36
|
from hockey_blast_common_lib.utils import (
|
|
37
37
|
assign_ranks,
|
|
38
|
+
calculate_percentile_value,
|
|
38
39
|
get_all_division_ids_for_org,
|
|
39
40
|
get_non_human_ids,
|
|
41
|
+
get_percentile_human,
|
|
40
42
|
get_start_datetime,
|
|
41
43
|
)
|
|
42
44
|
|
|
@@ -47,6 +49,67 @@ FORFEIT_STATUS = "FORFEIT"
|
|
|
47
49
|
NOEVENTS_STATUS = "NOEVENTS"
|
|
48
50
|
|
|
49
51
|
|
|
52
|
+
def insert_percentile_markers_goalie(
|
|
53
|
+
session, stats_dict, aggregation_id, total_in_rank, StatsModel
|
|
54
|
+
):
|
|
55
|
+
"""Insert percentile marker records for goalie stats.
|
|
56
|
+
|
|
57
|
+
For each stat field, calculate the 25th, 50th, 75th, 90th, and 95th percentile values
|
|
58
|
+
and insert marker records with fake human IDs.
|
|
59
|
+
"""
|
|
60
|
+
if not stats_dict:
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
# Define the stat fields we want to calculate percentiles for
|
|
64
|
+
stat_fields = [
|
|
65
|
+
"games_played",
|
|
66
|
+
"games_participated",
|
|
67
|
+
"games_with_stats",
|
|
68
|
+
"goals_allowed",
|
|
69
|
+
"shots_faced",
|
|
70
|
+
"goals_allowed_per_game",
|
|
71
|
+
"save_percentage",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
percentiles = [25, 50, 75, 90, 95]
|
|
75
|
+
|
|
76
|
+
for percentile in percentiles:
|
|
77
|
+
percentile_human_id = get_percentile_human(session, "Goalie", percentile)
|
|
78
|
+
|
|
79
|
+
percentile_values = {}
|
|
80
|
+
for field in stat_fields:
|
|
81
|
+
values = [stat[field] for stat in stats_dict.values() if field in stat]
|
|
82
|
+
if values:
|
|
83
|
+
percentile_values[field] = calculate_percentile_value(values, percentile)
|
|
84
|
+
else:
|
|
85
|
+
percentile_values[field] = 0
|
|
86
|
+
|
|
87
|
+
goalie_stat = StatsModel(
|
|
88
|
+
aggregation_id=aggregation_id,
|
|
89
|
+
human_id=percentile_human_id,
|
|
90
|
+
games_played=int(percentile_values.get("games_played", 0)),
|
|
91
|
+
games_participated=int(percentile_values.get("games_participated", 0)),
|
|
92
|
+
games_participated_rank=0,
|
|
93
|
+
games_with_stats=int(percentile_values.get("games_with_stats", 0)),
|
|
94
|
+
games_with_stats_rank=0,
|
|
95
|
+
goals_allowed=int(percentile_values.get("goals_allowed", 0)),
|
|
96
|
+
shots_faced=int(percentile_values.get("shots_faced", 0)),
|
|
97
|
+
goals_allowed_per_game=percentile_values.get("goals_allowed_per_game", 0.0),
|
|
98
|
+
save_percentage=percentile_values.get("save_percentage", 0.0),
|
|
99
|
+
games_played_rank=0,
|
|
100
|
+
goals_allowed_rank=0,
|
|
101
|
+
shots_faced_rank=0,
|
|
102
|
+
goals_allowed_per_game_rank=0,
|
|
103
|
+
save_percentage_rank=0,
|
|
104
|
+
total_in_rank=total_in_rank,
|
|
105
|
+
first_game_id=None,
|
|
106
|
+
last_game_id=None,
|
|
107
|
+
)
|
|
108
|
+
session.add(goalie_stat)
|
|
109
|
+
|
|
110
|
+
session.commit()
|
|
111
|
+
|
|
112
|
+
|
|
50
113
|
def aggregate_goalie_stats(
|
|
51
114
|
session,
|
|
52
115
|
aggregation_type,
|
|
@@ -54,6 +117,9 @@ def aggregate_goalie_stats(
|
|
|
54
117
|
debug_human_id=None,
|
|
55
118
|
aggregation_window=None,
|
|
56
119
|
):
|
|
120
|
+
# Capture start time for aggregation tracking
|
|
121
|
+
aggregation_start_time = datetime.utcnow()
|
|
122
|
+
|
|
57
123
|
human_ids_to_filter = get_non_human_ids(session)
|
|
58
124
|
|
|
59
125
|
# Get the name of the aggregation, for debug purposes
|
|
@@ -243,6 +309,11 @@ def aggregate_goalie_stats(
|
|
|
243
309
|
assign_ranks(stats_dict, "goals_allowed_per_game", reverse_rank=True)
|
|
244
310
|
assign_ranks(stats_dict, "save_percentage")
|
|
245
311
|
|
|
312
|
+
# Calculate and insert percentile marker records
|
|
313
|
+
insert_percentile_markers_goalie(
|
|
314
|
+
session, stats_dict, aggregation_id, total_in_rank, StatsModel
|
|
315
|
+
)
|
|
316
|
+
|
|
246
317
|
# Debug output for specific human
|
|
247
318
|
if debug_human_id:
|
|
248
319
|
if any(key[1] == debug_human_id for key in stats_dict):
|
|
@@ -296,6 +367,7 @@ def aggregate_goalie_stats(
|
|
|
296
367
|
total_in_rank=total_in_rank,
|
|
297
368
|
first_game_id=stat["first_game_id"],
|
|
298
369
|
last_game_id=stat["last_game_id"],
|
|
370
|
+
aggregation_started_at=aggregation_start_time,
|
|
299
371
|
)
|
|
300
372
|
session.add(goalie_stat)
|
|
301
373
|
# Commit in batches
|
|
@@ -303,6 +375,13 @@ def aggregate_goalie_stats(
|
|
|
303
375
|
session.commit()
|
|
304
376
|
session.commit()
|
|
305
377
|
|
|
378
|
+
# Update all records with completion timestamp
|
|
379
|
+
aggregation_end_time = datetime.utcnow()
|
|
380
|
+
session.query(StatsModel).filter(
|
|
381
|
+
StatsModel.aggregation_id == aggregation_id
|
|
382
|
+
).update({StatsModel.aggregation_completed_at: aggregation_end_time})
|
|
383
|
+
session.commit()
|
|
384
|
+
|
|
306
385
|
|
|
307
386
|
def run_aggregate_goalie_stats():
|
|
308
387
|
session = create_session("boss")
|
|
@@ -44,6 +44,9 @@ def aggregate_human_stats(
|
|
|
44
44
|
human_id_filter=None,
|
|
45
45
|
aggregation_window=None,
|
|
46
46
|
):
|
|
47
|
+
# Capture start time for aggregation tracking
|
|
48
|
+
aggregation_start_time = datetime.utcnow()
|
|
49
|
+
|
|
47
50
|
human_ids_to_filter = get_non_human_ids(session)
|
|
48
51
|
|
|
49
52
|
if aggregation_type == "org":
|
|
@@ -517,6 +520,7 @@ def aggregate_human_stats(
|
|
|
517
520
|
last_game_id_referee=stat["last_game_id_referee"],
|
|
518
521
|
first_game_id_scorekeeper=stat["first_game_id_scorekeeper"],
|
|
519
522
|
last_game_id_scorekeeper=stat["last_game_id_scorekeeper"],
|
|
523
|
+
aggregation_started_at=aggregation_start_time,
|
|
520
524
|
)
|
|
521
525
|
session.add(human_stat)
|
|
522
526
|
# Commit in batches
|
|
@@ -607,10 +611,18 @@ def aggregate_human_stats(
|
|
|
607
611
|
last_game_id_referee=overall_stats["last_game_id_referee"],
|
|
608
612
|
first_game_id_scorekeeper=overall_stats["first_game_id_scorekeeper"],
|
|
609
613
|
last_game_id_scorekeeper=overall_stats["last_game_id_scorekeeper"],
|
|
614
|
+
aggregation_started_at=aggregation_start_time,
|
|
610
615
|
)
|
|
611
616
|
session.add(overall_human_stat)
|
|
612
617
|
session.commit()
|
|
613
618
|
|
|
619
|
+
# Update all records with completion timestamp
|
|
620
|
+
aggregation_end_time = datetime.utcnow()
|
|
621
|
+
session.query(StatsModel).filter(
|
|
622
|
+
StatsModel.aggregation_id == aggregation_id
|
|
623
|
+
).update({StatsModel.aggregation_completed_at: aggregation_end_time})
|
|
624
|
+
session.commit()
|
|
625
|
+
|
|
614
626
|
|
|
615
627
|
def run_aggregate_human_stats():
|
|
616
628
|
session = create_session("boss")
|
|
@@ -5,6 +5,7 @@ import sys
|
|
|
5
5
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
from datetime import datetime
|
|
8
9
|
|
|
9
10
|
import sqlalchemy
|
|
10
11
|
from sqlalchemy.sql import case, func
|
|
@@ -29,8 +30,10 @@ from hockey_blast_common_lib.stats_models import (
|
|
|
29
30
|
from hockey_blast_common_lib.stats_utils import ALL_ORGS_ID
|
|
30
31
|
from hockey_blast_common_lib.utils import (
|
|
31
32
|
assign_ranks,
|
|
33
|
+
calculate_percentile_value,
|
|
32
34
|
get_all_division_ids_for_org,
|
|
33
35
|
get_non_human_ids,
|
|
36
|
+
get_percentile_human,
|
|
34
37
|
get_start_datetime,
|
|
35
38
|
)
|
|
36
39
|
|
|
@@ -41,9 +44,68 @@ FORFEIT_STATUS = "FORFEIT"
|
|
|
41
44
|
NOEVENTS_STATUS = "NOEVENTS"
|
|
42
45
|
|
|
43
46
|
|
|
47
|
+
def insert_percentile_markers_referee(
|
|
48
|
+
session, stats_dict, aggregation_id, total_in_rank, StatsModel
|
|
49
|
+
):
|
|
50
|
+
"""Insert percentile marker records for referee stats."""
|
|
51
|
+
if not stats_dict:
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
stat_fields = [
|
|
55
|
+
"games_reffed",
|
|
56
|
+
"games_participated",
|
|
57
|
+
"games_with_stats",
|
|
58
|
+
"penalties_given",
|
|
59
|
+
"penalties_per_game",
|
|
60
|
+
"gm_given",
|
|
61
|
+
"gm_per_game",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
percentiles = [25, 50, 75, 90, 95]
|
|
65
|
+
|
|
66
|
+
for percentile in percentiles:
|
|
67
|
+
percentile_human_id = get_percentile_human(session, "Ref", percentile)
|
|
68
|
+
|
|
69
|
+
percentile_values = {}
|
|
70
|
+
for field in stat_fields:
|
|
71
|
+
values = [stat[field] for stat in stats_dict.values() if field in stat]
|
|
72
|
+
if values:
|
|
73
|
+
percentile_values[field] = calculate_percentile_value(values, percentile)
|
|
74
|
+
else:
|
|
75
|
+
percentile_values[field] = 0
|
|
76
|
+
|
|
77
|
+
referee_stat = StatsModel(
|
|
78
|
+
aggregation_id=aggregation_id,
|
|
79
|
+
human_id=percentile_human_id,
|
|
80
|
+
games_reffed=int(percentile_values.get("games_reffed", 0)),
|
|
81
|
+
games_participated=int(percentile_values.get("games_participated", 0)),
|
|
82
|
+
games_participated_rank=0,
|
|
83
|
+
games_with_stats=int(percentile_values.get("games_with_stats", 0)),
|
|
84
|
+
games_with_stats_rank=0,
|
|
85
|
+
penalties_given=int(percentile_values.get("penalties_given", 0)),
|
|
86
|
+
penalties_per_game=percentile_values.get("penalties_per_game", 0.0),
|
|
87
|
+
gm_given=int(percentile_values.get("gm_given", 0)),
|
|
88
|
+
gm_per_game=percentile_values.get("gm_per_game", 0.0),
|
|
89
|
+
games_reffed_rank=0,
|
|
90
|
+
penalties_given_rank=0,
|
|
91
|
+
penalties_per_game_rank=0,
|
|
92
|
+
gm_given_rank=0,
|
|
93
|
+
gm_per_game_rank=0,
|
|
94
|
+
total_in_rank=total_in_rank,
|
|
95
|
+
first_game_id=None,
|
|
96
|
+
last_game_id=None,
|
|
97
|
+
)
|
|
98
|
+
session.add(referee_stat)
|
|
99
|
+
|
|
100
|
+
session.commit()
|
|
101
|
+
|
|
102
|
+
|
|
44
103
|
def aggregate_referee_stats(
|
|
45
104
|
session, aggregation_type, aggregation_id, aggregation_window=None
|
|
46
105
|
):
|
|
106
|
+
# Capture start time for aggregation tracking
|
|
107
|
+
aggregation_start_time = datetime.utcnow()
|
|
108
|
+
|
|
47
109
|
human_ids_to_filter = get_non_human_ids(session)
|
|
48
110
|
|
|
49
111
|
if aggregation_type == "org":
|
|
@@ -281,6 +343,11 @@ def aggregate_referee_stats(
|
|
|
281
343
|
assign_ranks(stats_dict, "gm_given")
|
|
282
344
|
assign_ranks(stats_dict, "gm_per_game")
|
|
283
345
|
|
|
346
|
+
# Calculate and insert percentile marker records
|
|
347
|
+
insert_percentile_markers_referee(
|
|
348
|
+
session, stats_dict, aggregation_id, total_in_rank, StatsModel
|
|
349
|
+
)
|
|
350
|
+
|
|
284
351
|
# Insert aggregated stats into the appropriate table with progress output
|
|
285
352
|
total_items = len(stats_dict)
|
|
286
353
|
batch_size = 1000
|
|
@@ -312,6 +379,7 @@ def aggregate_referee_stats(
|
|
|
312
379
|
total_in_rank=total_in_rank,
|
|
313
380
|
first_game_id=stat["first_game_id"],
|
|
314
381
|
last_game_id=stat["last_game_id"],
|
|
382
|
+
aggregation_started_at=aggregation_start_time,
|
|
315
383
|
)
|
|
316
384
|
session.add(referee_stat)
|
|
317
385
|
# Commit in batches
|
|
@@ -319,6 +387,13 @@ def aggregate_referee_stats(
|
|
|
319
387
|
session.commit()
|
|
320
388
|
session.commit()
|
|
321
389
|
|
|
390
|
+
# Update all records with completion timestamp
|
|
391
|
+
aggregation_end_time = datetime.utcnow()
|
|
392
|
+
session.query(StatsModel).filter(
|
|
393
|
+
StatsModel.aggregation_id == aggregation_id
|
|
394
|
+
).update({StatsModel.aggregation_completed_at: aggregation_end_time})
|
|
395
|
+
session.commit()
|
|
396
|
+
|
|
322
397
|
|
|
323
398
|
def run_aggregate_referee_stats():
|
|
324
399
|
session = create_session("boss")
|