hockey-blast-common-lib 0.1.65__tar.gz → 0.1.67__tar.gz
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-0.1.65 → hockey_blast_common_lib-0.1.67}/PKG-INFO +1 -1
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/aggregate_all_stats.py +14 -0
- hockey_blast_common_lib-0.1.67/hockey_blast_common_lib/aggregate_game_stats_all.py +150 -0
- hockey_blast_common_lib-0.1.67/hockey_blast_common_lib/aggregate_game_stats_goalie.py +257 -0
- hockey_blast_common_lib-0.1.67/hockey_blast_common_lib/aggregate_game_stats_skater.py +361 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/aggregate_goalie_stats.py +79 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/aggregate_human_stats.py +12 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/aggregate_referee_stats.py +75 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/aggregate_scorekeeper_stats.py +91 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/aggregate_skater_stats.py +118 -0
- hockey_blast_common_lib-0.1.67/hockey_blast_common_lib/aggregate_team_goalie_stats.py +265 -0
- hockey_blast_common_lib-0.1.67/hockey_blast_common_lib/aggregate_team_skater_stats.py +313 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/hockey_blast_sample_backup.sql.gz +0 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/models.py +16 -1
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/stats_models.py +211 -5
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/utils.py +65 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib.egg-info/PKG-INFO +1 -1
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib.egg-info/SOURCES.txt +5 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/setup.py +1 -1
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/MANIFEST.in +0 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/README.md +0 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/__init__.py +0 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/aggregate_h2h_stats.py +0 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/aggregate_s2s_stats.py +0 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/assign_skater_skill.py +0 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/db_connection.py +0 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/dump_sample_db.sh +0 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/embedding_utils.py +0 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/h2h_models.py +0 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/options.py +0 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/progress_utils.py +0 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/restore_sample_db.sh +0 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/skills_in_divisions.py +0 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/skills_propagation.py +0 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/stats_utils.py +0 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/wsgi.py +0 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib.egg-info/dependency_links.txt +0 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib.egg-info/requires.txt +0 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib.egg-info/top_level.txt +0 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/pyproject.toml +0 -0
- {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/setup.cfg +0 -0
|
@@ -11,6 +11,12 @@ from hockey_blast_common_lib.aggregate_scorekeeper_stats import (
|
|
|
11
11
|
run_aggregate_scorekeeper_stats,
|
|
12
12
|
)
|
|
13
13
|
from hockey_blast_common_lib.aggregate_skater_stats import run_aggregate_skater_stats
|
|
14
|
+
from hockey_blast_common_lib.aggregate_team_goalie_stats import (
|
|
15
|
+
run_aggregate_team_goalie_stats,
|
|
16
|
+
)
|
|
17
|
+
from hockey_blast_common_lib.aggregate_team_skater_stats import (
|
|
18
|
+
run_aggregate_team_skater_stats,
|
|
19
|
+
)
|
|
14
20
|
|
|
15
21
|
if __name__ == "__main__":
|
|
16
22
|
print("Running aggregate_skater_stats...", flush=True)
|
|
@@ -32,3 +38,11 @@ if __name__ == "__main__":
|
|
|
32
38
|
print("Running aggregate_human_stats...", flush=True)
|
|
33
39
|
run_aggregate_human_stats()
|
|
34
40
|
print("Finished running aggregate_human_stats\n")
|
|
41
|
+
|
|
42
|
+
print("Running aggregate_team_skater_stats...", flush=True)
|
|
43
|
+
run_aggregate_team_skater_stats()
|
|
44
|
+
print("Finished running aggregate_team_skater_stats\n")
|
|
45
|
+
|
|
46
|
+
print("Running aggregate_team_goalie_stats...", flush=True)
|
|
47
|
+
run_aggregate_team_goalie_stats()
|
|
48
|
+
print("Finished running aggregate_team_goalie_stats\n")
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Orchestrator for per-game statistics aggregation.
|
|
4
|
+
|
|
5
|
+
This script provides a unified interface for running both skater and goalie
|
|
6
|
+
per-game statistics aggregation. It supports full and append modes and can
|
|
7
|
+
run aggregations for specific roles or all roles together.
|
|
8
|
+
|
|
9
|
+
Usage examples:
|
|
10
|
+
# Full regeneration of all per-game stats
|
|
11
|
+
python aggregate_game_stats_all.py --mode full --role all
|
|
12
|
+
|
|
13
|
+
# Append new games for skaters only
|
|
14
|
+
python aggregate_game_stats_all.py --mode append --role skater
|
|
15
|
+
|
|
16
|
+
# Append new games for goalies only
|
|
17
|
+
python aggregate_game_stats_all.py --mode append --role goalie
|
|
18
|
+
|
|
19
|
+
The script automatically manages sentinel record tracking across both stat types
|
|
20
|
+
to ensure consistent append mode behavior.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
from datetime import datetime
|
|
27
|
+
|
|
28
|
+
# Add the package directory to the Python path
|
|
29
|
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
30
|
+
|
|
31
|
+
from hockey_blast_common_lib.aggregate_game_stats_goalie import aggregate_game_stats_goalie
|
|
32
|
+
from hockey_blast_common_lib.aggregate_game_stats_skater import aggregate_game_stats_skater
|
|
33
|
+
from hockey_blast_common_lib.db_connection import create_session
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def run_aggregation(mode="full", role="all", human_id=None):
|
|
37
|
+
"""Run per-game statistics aggregation.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
mode: "full" to regenerate all records, "append" to process new games only
|
|
41
|
+
role: "skater", "goalie", or "all" to specify which stats to aggregate
|
|
42
|
+
human_id: Optional human_id to process only one player (for testing/debugging)
|
|
43
|
+
"""
|
|
44
|
+
session = create_session("boss")
|
|
45
|
+
|
|
46
|
+
start_time = datetime.now()
|
|
47
|
+
print(f"\n{'='*80}")
|
|
48
|
+
print(f"PER-GAME STATISTICS AGGREGATION")
|
|
49
|
+
print(f"Mode: {mode.upper()}")
|
|
50
|
+
print(f"Role: {role.upper()}")
|
|
51
|
+
if human_id:
|
|
52
|
+
print(f"Human ID Filter: {human_id}")
|
|
53
|
+
print(f"Started: {start_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
54
|
+
print(f"{'='*80}\n")
|
|
55
|
+
|
|
56
|
+
# Run skater aggregation
|
|
57
|
+
if role in ["skater", "all"]:
|
|
58
|
+
try:
|
|
59
|
+
aggregate_game_stats_skater(session, mode=mode, human_id=human_id)
|
|
60
|
+
except Exception as e:
|
|
61
|
+
print(f"\nERROR: Skater aggregation failed: {e}")
|
|
62
|
+
import traceback
|
|
63
|
+
traceback.print_exc()
|
|
64
|
+
if role == "skater":
|
|
65
|
+
sys.exit(1)
|
|
66
|
+
# If running all, continue to goalie even if skater fails
|
|
67
|
+
print("\nContinuing to goalie aggregation...\n")
|
|
68
|
+
|
|
69
|
+
# Run goalie aggregation
|
|
70
|
+
if role in ["goalie", "all"]:
|
|
71
|
+
try:
|
|
72
|
+
aggregate_game_stats_goalie(session, mode=mode, human_id=human_id)
|
|
73
|
+
except Exception as e:
|
|
74
|
+
print(f"\nERROR: Goalie aggregation failed: {e}")
|
|
75
|
+
import traceback
|
|
76
|
+
traceback.print_exc()
|
|
77
|
+
sys.exit(1)
|
|
78
|
+
|
|
79
|
+
end_time = datetime.now()
|
|
80
|
+
duration = end_time - start_time
|
|
81
|
+
|
|
82
|
+
print(f"\n{'='*80}")
|
|
83
|
+
print(f"AGGREGATION COMPLETE")
|
|
84
|
+
print(f"Duration: {duration}")
|
|
85
|
+
print(f"Finished: {end_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
86
|
+
print(f"{'='*80}\n")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def main():
|
|
90
|
+
"""Main entry point for CLI."""
|
|
91
|
+
parser = argparse.ArgumentParser(
|
|
92
|
+
description="Aggregate per-game statistics for skaters and goalies",
|
|
93
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
94
|
+
epilog="""
|
|
95
|
+
Examples:
|
|
96
|
+
# Full regeneration of all stats
|
|
97
|
+
%(prog)s --mode full --role all
|
|
98
|
+
|
|
99
|
+
# Append new games for skaters only
|
|
100
|
+
%(prog)s --mode append --role skater
|
|
101
|
+
|
|
102
|
+
# Append new games for goalies only
|
|
103
|
+
%(prog)s --mode append --role goalie
|
|
104
|
+
|
|
105
|
+
Notes:
|
|
106
|
+
- Full mode deletes and regenerates all records
|
|
107
|
+
- Append mode uses sentinel tracking with 1-day overlap
|
|
108
|
+
- Only saves non-zero records (RAG optimization)
|
|
109
|
+
- Skater stats: saves games with goals, assists, or penalties
|
|
110
|
+
- Goalie stats: saves games where goalie faced shots
|
|
111
|
+
""",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
parser.add_argument(
|
|
115
|
+
"--mode",
|
|
116
|
+
choices=["full", "append"],
|
|
117
|
+
default="full",
|
|
118
|
+
help="Aggregation mode (default: full)",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
parser.add_argument(
|
|
122
|
+
"--role",
|
|
123
|
+
choices=["skater", "goalie", "all"],
|
|
124
|
+
default="all",
|
|
125
|
+
help="Which role stats to aggregate (default: all)",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
parser.add_argument(
|
|
129
|
+
"--human-id",
|
|
130
|
+
type=int,
|
|
131
|
+
default=None,
|
|
132
|
+
help="Optional: Limit processing to specific human_id (for testing)",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
args = parser.parse_args()
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
run_aggregation(mode=args.mode, role=args.role, human_id=args.human_id)
|
|
139
|
+
except KeyboardInterrupt:
|
|
140
|
+
print("\n\nAggregation cancelled by user.")
|
|
141
|
+
sys.exit(130)
|
|
142
|
+
except Exception as e:
|
|
143
|
+
print(f"\n\nFATAL ERROR: {e}")
|
|
144
|
+
import traceback
|
|
145
|
+
traceback.print_exc()
|
|
146
|
+
sys.exit(1)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
main()
|
|
@@ -0,0 +1,257 @@
|
|
|
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_, 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, GoalieSaves, Human
|
|
14
|
+
from hockey_blast_common_lib.progress_utils import create_progress_tracker
|
|
15
|
+
from hockey_blast_common_lib.stats_models import GameStatsGoalie
|
|
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_goalie(session, mode="full", human_id=None):
|
|
24
|
+
"""Aggregate per-game goalie 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 goalie (for testing/debugging)
|
|
30
|
+
|
|
31
|
+
The function stores individual game performance for each goalie with non-zero stats.
|
|
32
|
+
Only games where the goalie faced at least one shot 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
|
+
Note: Uses GameStatsGoalie table but shares sentinel tracking with GameStatsSkater
|
|
39
|
+
since both are per-game stats that should be processed together.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
# Get Incognito Human for sentinel tracking (first_name="Incognito", middle_name="", last_name="Human")
|
|
43
|
+
incognito_human = session.query(Human).filter_by(
|
|
44
|
+
first_name="Incognito", middle_name="", last_name="Human"
|
|
45
|
+
).first()
|
|
46
|
+
if not incognito_human:
|
|
47
|
+
raise RuntimeError("Incognito Human not found in database - required for sentinel tracking")
|
|
48
|
+
incognito_human_id = incognito_human.id
|
|
49
|
+
|
|
50
|
+
non_human_ids = get_non_human_ids(session)
|
|
51
|
+
|
|
52
|
+
# Add human_id to filter if specified
|
|
53
|
+
if human_id:
|
|
54
|
+
human = session.query(Human).filter_by(id=human_id).first()
|
|
55
|
+
if not human:
|
|
56
|
+
print(f"ERROR: Human ID {human_id} not found in database")
|
|
57
|
+
return
|
|
58
|
+
print(f"Limiting to human_id={human_id}: {human.first_name} {human.last_name}\n")
|
|
59
|
+
|
|
60
|
+
print(f"\n{'='*80}")
|
|
61
|
+
print(f"Aggregating per-game goalie statistics (mode: {mode})")
|
|
62
|
+
print(f"{'='*80}\n")
|
|
63
|
+
|
|
64
|
+
# Determine game filtering based on mode
|
|
65
|
+
# Note: We check GameStatsSkater for sentinel since they're processed together
|
|
66
|
+
if mode == "append":
|
|
67
|
+
# Import here to avoid circular dependency
|
|
68
|
+
from hockey_blast_common_lib.stats_models import GameStatsSkater
|
|
69
|
+
|
|
70
|
+
# Query sentinel record for last processed timestamp
|
|
71
|
+
sentinel = (
|
|
72
|
+
session.query(GameStatsSkater)
|
|
73
|
+
.filter(
|
|
74
|
+
GameStatsSkater.human_id == incognito_human_id,
|
|
75
|
+
GameStatsSkater.game_id == -1,
|
|
76
|
+
)
|
|
77
|
+
.first()
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if sentinel:
|
|
81
|
+
last_processed = datetime.combine(sentinel.game_date, sentinel.game_time)
|
|
82
|
+
# Subtract 1 day for overlap to catch data corrections
|
|
83
|
+
start_datetime = last_processed - timedelta(days=1)
|
|
84
|
+
print(f"Append mode: Processing games after {start_datetime}")
|
|
85
|
+
print(f"(1-day overlap from last processed: {last_processed})\n")
|
|
86
|
+
|
|
87
|
+
# Delete records for games in the overlap window
|
|
88
|
+
delete_count = (
|
|
89
|
+
session.query(GameStatsGoalie)
|
|
90
|
+
.filter(
|
|
91
|
+
GameStatsGoalie.human_id != incognito_human_id,
|
|
92
|
+
func.cast(
|
|
93
|
+
func.concat(GameStatsGoalie.game_date, " ", GameStatsGoalie.game_time),
|
|
94
|
+
func.TIMESTAMP,
|
|
95
|
+
) >= start_datetime,
|
|
96
|
+
)
|
|
97
|
+
.delete(synchronize_session=False)
|
|
98
|
+
)
|
|
99
|
+
session.commit()
|
|
100
|
+
print(f"Deleted {delete_count} existing records in overlap window\n")
|
|
101
|
+
else:
|
|
102
|
+
# No sentinel found, treat as full mode
|
|
103
|
+
print("No sentinel record found - treating as full mode\n")
|
|
104
|
+
mode = "full"
|
|
105
|
+
start_datetime = None
|
|
106
|
+
else:
|
|
107
|
+
start_datetime = None
|
|
108
|
+
|
|
109
|
+
if mode == "full":
|
|
110
|
+
# Delete all existing records (no sentinel for goalie table)
|
|
111
|
+
delete_count = session.query(GameStatsGoalie).delete(synchronize_session=False)
|
|
112
|
+
session.commit()
|
|
113
|
+
print(f"Full mode: Deleted {delete_count} existing records\n")
|
|
114
|
+
|
|
115
|
+
# Build game filter for eligible games
|
|
116
|
+
game_filter = Game.status.in_([FINAL_STATUS, FINAL_SO_STATUS])
|
|
117
|
+
if mode == "append" and start_datetime:
|
|
118
|
+
game_filter = and_(
|
|
119
|
+
game_filter,
|
|
120
|
+
func.cast(
|
|
121
|
+
func.concat(Game.date, " ", Game.time),
|
|
122
|
+
func.TIMESTAMP,
|
|
123
|
+
) >= start_datetime,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Count total GoalieSaves records to process for progress tracking
|
|
127
|
+
total_saves_records = (
|
|
128
|
+
session.query(GoalieSaves)
|
|
129
|
+
.join(Game, GoalieSaves.game_id == Game.id)
|
|
130
|
+
.filter(game_filter)
|
|
131
|
+
.count()
|
|
132
|
+
)
|
|
133
|
+
print(f"Processing {total_saves_records} goalie save records...\n")
|
|
134
|
+
|
|
135
|
+
if total_saves_records == 0:
|
|
136
|
+
print("No goalie records to process.\n")
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
# Query goalie saves joined with game metadata and roster
|
|
140
|
+
# GoalieSaves already has per-game goalie data
|
|
141
|
+
goalie_query = (
|
|
142
|
+
session.query(
|
|
143
|
+
GoalieSaves.game_id,
|
|
144
|
+
GoalieSaves.goalie_id.label("human_id"),
|
|
145
|
+
GameRoster.team_id,
|
|
146
|
+
Game.org_id,
|
|
147
|
+
Division.level_id,
|
|
148
|
+
Game.date.label("game_date"),
|
|
149
|
+
Game.time.label("game_time"),
|
|
150
|
+
GoalieSaves.goals_allowed,
|
|
151
|
+
GoalieSaves.shots_against.label("shots_faced"),
|
|
152
|
+
GoalieSaves.saves_count.label("saves"),
|
|
153
|
+
)
|
|
154
|
+
.join(Game, GoalieSaves.game_id == Game.id)
|
|
155
|
+
.join(Division, Game.division_id == Division.id)
|
|
156
|
+
.join(
|
|
157
|
+
GameRoster,
|
|
158
|
+
and_(
|
|
159
|
+
GameRoster.game_id == GoalieSaves.game_id,
|
|
160
|
+
GameRoster.human_id == GoalieSaves.goalie_id,
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
.filter(
|
|
164
|
+
game_filter,
|
|
165
|
+
GoalieSaves.goalie_id.notin_(non_human_ids), # Filter placeholder humans
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Add human_id filter if specified
|
|
170
|
+
if human_id:
|
|
171
|
+
goalie_query = goalie_query.filter(GoalieSaves.goalie_id == human_id)
|
|
172
|
+
|
|
173
|
+
goalie_records = goalie_query.all()
|
|
174
|
+
|
|
175
|
+
print(f"Found {len(goalie_records)} goalie save records\n")
|
|
176
|
+
|
|
177
|
+
# Filter to only non-zero stats (CRITICAL for RAG efficiency)
|
|
178
|
+
# Only save records where goalie faced at least one shot
|
|
179
|
+
print("Filtering to non-zero records...")
|
|
180
|
+
nonzero_records = [record for record in goalie_records if record.shots_faced > 0]
|
|
181
|
+
|
|
182
|
+
print(f"Filtered: {len(nonzero_records)} non-zero records (from {len(goalie_records)} total)\n")
|
|
183
|
+
|
|
184
|
+
# Insert records in batches with progress tracking
|
|
185
|
+
batch_size = 1000
|
|
186
|
+
total_records = len(nonzero_records)
|
|
187
|
+
|
|
188
|
+
if total_records == 0:
|
|
189
|
+
print("No non-zero records to insert.\n")
|
|
190
|
+
else:
|
|
191
|
+
progress = create_progress_tracker(total_records, "Inserting per-game goalie stats")
|
|
192
|
+
|
|
193
|
+
records_to_insert = []
|
|
194
|
+
for i, record in enumerate(nonzero_records, 1):
|
|
195
|
+
# Calculate save percentage
|
|
196
|
+
if record.shots_faced > 0:
|
|
197
|
+
save_percentage = (record.shots_faced - record.goals_allowed) / record.shots_faced
|
|
198
|
+
else:
|
|
199
|
+
save_percentage = 0.0
|
|
200
|
+
|
|
201
|
+
game_stats_record = GameStatsGoalie(
|
|
202
|
+
game_id=record.game_id,
|
|
203
|
+
human_id=record.human_id,
|
|
204
|
+
team_id=record.team_id,
|
|
205
|
+
org_id=record.org_id,
|
|
206
|
+
level_id=record.level_id,
|
|
207
|
+
game_date=record.game_date,
|
|
208
|
+
game_time=record.game_time,
|
|
209
|
+
goals_allowed=record.goals_allowed,
|
|
210
|
+
shots_faced=record.shots_faced,
|
|
211
|
+
saves=record.saves,
|
|
212
|
+
save_percentage=save_percentage,
|
|
213
|
+
created_at=datetime.utcnow(),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
records_to_insert.append(game_stats_record)
|
|
217
|
+
|
|
218
|
+
# Commit in batches
|
|
219
|
+
if i % batch_size == 0 or i == total_records:
|
|
220
|
+
session.bulk_save_objects(records_to_insert)
|
|
221
|
+
session.commit()
|
|
222
|
+
records_to_insert = []
|
|
223
|
+
progress.update(i)
|
|
224
|
+
|
|
225
|
+
print("\nInsert complete.\n")
|
|
226
|
+
|
|
227
|
+
print(f"\n{'='*80}")
|
|
228
|
+
print("Per-game goalie statistics aggregation complete")
|
|
229
|
+
print(f"{'='*80}\n")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def run_aggregate_game_stats_goalie():
|
|
233
|
+
"""Main entry point for goalie per-game aggregation."""
|
|
234
|
+
import argparse
|
|
235
|
+
|
|
236
|
+
parser = argparse.ArgumentParser(description="Aggregate per-game goalie statistics")
|
|
237
|
+
parser.add_argument(
|
|
238
|
+
"--mode",
|
|
239
|
+
choices=["full", "append"],
|
|
240
|
+
default="full",
|
|
241
|
+
help="Aggregation mode: 'full' to regenerate all, 'append' to add new games only",
|
|
242
|
+
)
|
|
243
|
+
parser.add_argument(
|
|
244
|
+
"--human-id",
|
|
245
|
+
type=int,
|
|
246
|
+
default=None,
|
|
247
|
+
help="Optional: Limit processing to specific human_id (for testing)",
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
args = parser.parse_args()
|
|
251
|
+
|
|
252
|
+
session = create_session("boss")
|
|
253
|
+
aggregate_game_stats_goalie(session, mode=args.mode, human_id=args.human_id)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
if __name__ == "__main__":
|
|
257
|
+
run_aggregate_game_stats_goalie()
|