hockey-blast-common-lib 0.1.66__py3-none-any.whl → 0.1.68__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_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 +11 -0
- hockey_blast_common_lib/aggregate_human_stats.py +12 -0
- hockey_blast_common_lib/aggregate_referee_stats.py +12 -0
- hockey_blast_common_lib/aggregate_scorekeeper_stats.py +13 -0
- hockey_blast_common_lib/aggregate_skater_stats.py +142 -26
- hockey_blast_common_lib/aggregate_team_goalie_stats.py +14 -0
- hockey_blast_common_lib/aggregate_team_skater_stats.py +14 -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 +88 -5
- hockey_blast_common_lib/utils.py +2 -1
- {hockey_blast_common_lib-0.1.66.dist-info → hockey_blast_common_lib-0.1.68.dist-info}/METADATA +1 -1
- {hockey_blast_common_lib-0.1.66.dist-info → hockey_blast_common_lib-0.1.68.dist-info}/RECORD +18 -15
- {hockey_blast_common_lib-0.1.66.dist-info → hockey_blast_common_lib-0.1.68.dist-info}/WHEEL +0 -0
- {hockey_blast_common_lib-0.1.66.dist-info → hockey_blast_common_lib-0.1.68.dist-info}/top_level.txt +0 -0
|
@@ -6,6 +6,7 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
import sqlalchemy
|
|
9
|
+
from datetime import datetime
|
|
9
10
|
from sqlalchemy import and_, case, func
|
|
10
11
|
from sqlalchemy.sql import case, func
|
|
11
12
|
|
|
@@ -117,6 +118,118 @@ def calculate_current_point_streak(session, human_id, filter_condition):
|
|
|
117
118
|
return current_streak, avg_points_during_streak
|
|
118
119
|
|
|
119
120
|
|
|
121
|
+
def calculate_all_point_streaks_batch(session, human_ids, filter_condition):
|
|
122
|
+
"""
|
|
123
|
+
Calculate point streaks for ALL players in one batch query.
|
|
124
|
+
This is MUCH faster than calling calculate_current_point_streak() for each player.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
session: Database session
|
|
128
|
+
human_ids: List of all human_ids to calculate streaks for (e.g., 150 players in a division)
|
|
129
|
+
filter_condition: SQLAlchemy filter condition (org/division/level + time window)
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Dict[human_id] -> (streak_length, avg_points_per_game)
|
|
133
|
+
|
|
134
|
+
Performance:
|
|
135
|
+
- Old: N queries (one per player)
|
|
136
|
+
- New: 1 query for all players
|
|
137
|
+
- Speedup: ~150x for typical division
|
|
138
|
+
"""
|
|
139
|
+
if not human_ids:
|
|
140
|
+
return {}
|
|
141
|
+
|
|
142
|
+
# Single query for ALL players at once
|
|
143
|
+
game_points = (
|
|
144
|
+
session.query(
|
|
145
|
+
GameRoster.human_id,
|
|
146
|
+
Game.id.label("game_id"),
|
|
147
|
+
Game.date,
|
|
148
|
+
Game.time,
|
|
149
|
+
func.sum(
|
|
150
|
+
case((Goal.goal_scorer_id == GameRoster.human_id, 1), else_=0)
|
|
151
|
+
).label("goals"),
|
|
152
|
+
func.sum(
|
|
153
|
+
case(
|
|
154
|
+
(
|
|
155
|
+
(Goal.assist_1_id == GameRoster.human_id) |
|
|
156
|
+
(Goal.assist_2_id == GameRoster.human_id),
|
|
157
|
+
1
|
|
158
|
+
),
|
|
159
|
+
else_=0
|
|
160
|
+
)
|
|
161
|
+
).label("assists"),
|
|
162
|
+
)
|
|
163
|
+
.join(Game, GameRoster.game_id == Game.id)
|
|
164
|
+
.outerjoin(Goal, Game.id == Goal.game_id)
|
|
165
|
+
.filter(
|
|
166
|
+
GameRoster.human_id.in_(human_ids), # ← ALL players at once!
|
|
167
|
+
~GameRoster.role.ilike("g"), # Exclude goalie games
|
|
168
|
+
filter_condition,
|
|
169
|
+
(Game.status.like("Final%")) | (Game.status == "NOEVENTS"),
|
|
170
|
+
)
|
|
171
|
+
.group_by(GameRoster.human_id, Game.id, Game.date, Game.time)
|
|
172
|
+
.order_by(
|
|
173
|
+
GameRoster.human_id, # Group by player first
|
|
174
|
+
Game.date.desc(), # Then most recent games first
|
|
175
|
+
Game.time.desc()
|
|
176
|
+
)
|
|
177
|
+
.all()
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Process results in Python to calculate streaks
|
|
181
|
+
streaks_by_human = {}
|
|
182
|
+
processed_humans = set() # Track which humans we've finished processing
|
|
183
|
+
current_human = None
|
|
184
|
+
current_streak = 0
|
|
185
|
+
total_points = 0
|
|
186
|
+
|
|
187
|
+
for row in game_points:
|
|
188
|
+
# Skip if we've already processed this human (streak already broken)
|
|
189
|
+
if row.human_id in processed_humans:
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
# New player?
|
|
193
|
+
if row.human_id != current_human:
|
|
194
|
+
# Save previous player's streak (if any)
|
|
195
|
+
if current_human is not None:
|
|
196
|
+
avg = total_points / current_streak if current_streak > 0 else 0.0
|
|
197
|
+
streaks_by_human[current_human] = (current_streak, avg)
|
|
198
|
+
processed_humans.add(current_human)
|
|
199
|
+
|
|
200
|
+
# Reset for new player
|
|
201
|
+
current_human = row.human_id
|
|
202
|
+
current_streak = 0
|
|
203
|
+
total_points = 0
|
|
204
|
+
|
|
205
|
+
# Calculate points for this game
|
|
206
|
+
points = (row.goals or 0) + (row.assists or 0)
|
|
207
|
+
|
|
208
|
+
if points > 0:
|
|
209
|
+
# Streak continues
|
|
210
|
+
current_streak += 1
|
|
211
|
+
total_points += points
|
|
212
|
+
else:
|
|
213
|
+
# Streak broken - save and mark as processed
|
|
214
|
+
avg = total_points / current_streak if current_streak > 0 else 0.0
|
|
215
|
+
streaks_by_human[current_human] = (current_streak, avg)
|
|
216
|
+
processed_humans.add(current_human)
|
|
217
|
+
# Reset current_human so we skip remaining games for this player
|
|
218
|
+
current_human = None
|
|
219
|
+
|
|
220
|
+
# Don't forget the last player (if still in a streak)
|
|
221
|
+
if current_human is not None and current_human not in processed_humans:
|
|
222
|
+
avg = total_points / current_streak if current_streak > 0 else 0.0
|
|
223
|
+
streaks_by_human[current_human] = (current_streak, avg)
|
|
224
|
+
|
|
225
|
+
# Ensure all requested human_ids have a result (even if 0,0)
|
|
226
|
+
for human_id in human_ids:
|
|
227
|
+
if human_id not in streaks_by_human:
|
|
228
|
+
streaks_by_human[human_id] = (0, 0.0)
|
|
229
|
+
|
|
230
|
+
return streaks_by_human
|
|
231
|
+
|
|
232
|
+
|
|
120
233
|
def insert_percentile_markers_skater(
|
|
121
234
|
session, stats_dict, aggregation_id, total_in_rank, StatsModel, aggregation_window
|
|
122
235
|
):
|
|
@@ -223,6 +336,9 @@ def aggregate_skater_stats(
|
|
|
223
336
|
debug_human_id=None,
|
|
224
337
|
aggregation_window=None,
|
|
225
338
|
):
|
|
339
|
+
# Capture start time for aggregation tracking
|
|
340
|
+
aggregation_start_time = datetime.utcnow()
|
|
341
|
+
|
|
226
342
|
human_ids_to_filter = get_non_human_ids(session)
|
|
227
343
|
|
|
228
344
|
# Get the name of the aggregation, for debug purposes
|
|
@@ -601,34 +717,26 @@ def aggregate_skater_stats(
|
|
|
601
717
|
stat["last_game_id"] = last_game.id if last_game else None
|
|
602
718
|
|
|
603
719
|
# Calculate current point streak (only for all-time stats)
|
|
720
|
+
# OPTIMIZED: Use batch calculation instead of N+1 queries
|
|
604
721
|
if aggregation_window is None:
|
|
605
722
|
total_players = len(stats_dict)
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
)
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
progress.update(idx + 1)
|
|
624
|
-
else:
|
|
625
|
-
for key, stat in stats_dict.items():
|
|
626
|
-
agg_id, human_id = key
|
|
627
|
-
streak_length, avg_points = calculate_current_point_streak(
|
|
628
|
-
session, human_id, filter_condition
|
|
629
|
-
)
|
|
630
|
-
stat["current_point_streak"] = streak_length
|
|
631
|
-
stat["current_point_streak_avg_points"] = avg_points
|
|
723
|
+
|
|
724
|
+
# Extract all human_ids from stats_dict
|
|
725
|
+
all_human_ids = [key[1] for key in stats_dict.keys()]
|
|
726
|
+
|
|
727
|
+
# Calculate all point streaks in ONE batch query (instead of N queries)
|
|
728
|
+
print(f"Calculating point streaks for {total_players} players using batch query...")
|
|
729
|
+
all_streaks = calculate_all_point_streaks_batch(
|
|
730
|
+
session, all_human_ids, filter_condition
|
|
731
|
+
)
|
|
732
|
+
print(f"✓ Point streaks calculated for {len(all_streaks)} players")
|
|
733
|
+
|
|
734
|
+
# Assign streak values to stats_dict
|
|
735
|
+
for key, stat in stats_dict.items():
|
|
736
|
+
agg_id, human_id = key
|
|
737
|
+
streak_length, avg_points = all_streaks.get(human_id, (0, 0.0))
|
|
738
|
+
stat["current_point_streak"] = streak_length
|
|
739
|
+
stat["current_point_streak_avg_points"] = avg_points
|
|
632
740
|
|
|
633
741
|
# Calculate total_in_rank
|
|
634
742
|
total_in_rank = len(stats_dict)
|
|
@@ -754,6 +862,7 @@ def aggregate_skater_stats(
|
|
|
754
862
|
),
|
|
755
863
|
first_game_id=stat["first_game_id"],
|
|
756
864
|
last_game_id=stat["last_game_id"],
|
|
865
|
+
aggregation_started_at=aggregation_start_time,
|
|
757
866
|
)
|
|
758
867
|
session.add(skater_stat)
|
|
759
868
|
# Commit in batches
|
|
@@ -761,6 +870,13 @@ def aggregate_skater_stats(
|
|
|
761
870
|
session.commit()
|
|
762
871
|
session.commit()
|
|
763
872
|
|
|
873
|
+
# Update all records with completion timestamp
|
|
874
|
+
aggregation_end_time = datetime.utcnow()
|
|
875
|
+
session.query(StatsModel).filter(
|
|
876
|
+
StatsModel.aggregation_id == aggregation_id
|
|
877
|
+
).update({StatsModel.aggregation_completed_at: aggregation_end_time})
|
|
878
|
+
session.commit()
|
|
879
|
+
|
|
764
880
|
|
|
765
881
|
def run_aggregate_skater_stats():
|
|
766
882
|
session = create_session("boss")
|
|
@@ -15,6 +15,8 @@ import sys
|
|
|
15
15
|
|
|
16
16
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
17
17
|
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
|
|
18
20
|
import sqlalchemy
|
|
19
21
|
from sqlalchemy import func
|
|
20
22
|
|
|
@@ -62,6 +64,9 @@ def aggregate_team_goalie_stats(session, aggregation_type, aggregation_id):
|
|
|
62
64
|
aggregation_type: "org" or "division"
|
|
63
65
|
aggregation_id: ID of the organization or division
|
|
64
66
|
"""
|
|
67
|
+
# Capture start time for aggregation tracking
|
|
68
|
+
aggregation_start_time = datetime.utcnow()
|
|
69
|
+
|
|
65
70
|
human_ids_to_filter = get_non_human_ids(session)
|
|
66
71
|
|
|
67
72
|
# Determine aggregation details
|
|
@@ -221,10 +226,19 @@ def aggregate_team_goalie_stats(session, aggregation_type, aggregation_id):
|
|
|
221
226
|
shots_faced_rank=0,
|
|
222
227
|
goals_allowed_per_game_rank=0,
|
|
223
228
|
save_percentage_rank=0,
|
|
229
|
+
aggregation_started_at=aggregation_start_time,
|
|
224
230
|
)
|
|
225
231
|
session.add(goalie_stat)
|
|
226
232
|
|
|
227
233
|
session.commit()
|
|
234
|
+
|
|
235
|
+
# Update all records with completion timestamp
|
|
236
|
+
aggregation_end_time = datetime.utcnow()
|
|
237
|
+
session.query(StatsModel).filter(
|
|
238
|
+
StatsModel.aggregation_id == aggregation_id
|
|
239
|
+
).update({StatsModel.aggregation_completed_at: aggregation_end_time})
|
|
240
|
+
session.commit()
|
|
241
|
+
|
|
228
242
|
progress.finish()
|
|
229
243
|
print(f"✓ Team goalie stats aggregation complete for {aggregation_name}")
|
|
230
244
|
|
|
@@ -15,6 +15,8 @@ import sys
|
|
|
15
15
|
|
|
16
16
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
17
17
|
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
|
|
18
20
|
import sqlalchemy
|
|
19
21
|
from sqlalchemy import and_, case, func
|
|
20
22
|
|
|
@@ -63,6 +65,9 @@ def aggregate_team_skater_stats(session, aggregation_type, aggregation_id):
|
|
|
63
65
|
aggregation_type: "org" or "division"
|
|
64
66
|
aggregation_id: ID of the organization or division
|
|
65
67
|
"""
|
|
68
|
+
# Capture start time for aggregation tracking
|
|
69
|
+
aggregation_start_time = datetime.utcnow()
|
|
70
|
+
|
|
66
71
|
human_ids_to_filter = get_non_human_ids(session)
|
|
67
72
|
|
|
68
73
|
# Determine aggregation details
|
|
@@ -269,10 +274,19 @@ def aggregate_team_skater_stats(session, aggregation_type, aggregation_id):
|
|
|
269
274
|
gm_penalties_per_game_rank=0,
|
|
270
275
|
current_point_streak_rank=0,
|
|
271
276
|
current_point_streak_avg_points_rank=0,
|
|
277
|
+
aggregation_started_at=aggregation_start_time,
|
|
272
278
|
)
|
|
273
279
|
session.add(skater_stat)
|
|
274
280
|
|
|
275
281
|
session.commit()
|
|
282
|
+
|
|
283
|
+
# Update all records with completion timestamp
|
|
284
|
+
aggregation_end_time = datetime.utcnow()
|
|
285
|
+
session.query(StatsModel).filter(
|
|
286
|
+
StatsModel.aggregation_id == aggregation_id
|
|
287
|
+
).update({StatsModel.aggregation_completed_at: aggregation_end_time})
|
|
288
|
+
session.commit()
|
|
289
|
+
|
|
276
290
|
progress.finish()
|
|
277
291
|
print(f"✓ Team skater stats aggregation complete for {aggregation_name}")
|
|
278
292
|
|
|
Binary file
|
|
@@ -53,7 +53,8 @@ class Game(db.Model):
|
|
|
53
53
|
time = db.Column(db.Time)
|
|
54
54
|
day_of_week = db.Column(db.Integer) # 1 to 7 for Monday to Sunday
|
|
55
55
|
period_length = db.Column(db.Integer) # In minutes
|
|
56
|
-
location = db.Column(db.String(100))
|
|
56
|
+
location = db.Column(db.String(100)) # DEPRECATED: Use location_id instead
|
|
57
|
+
location_id = db.Column(db.Integer, db.ForeignKey("locations.id"), nullable=True)
|
|
57
58
|
scorekeeper_id = db.Column(db.Integer, db.ForeignKey("humans.id"))
|
|
58
59
|
referee_1_id = db.Column(db.Integer, db.ForeignKey("humans.id"))
|
|
59
60
|
referee_2_id = db.Column(db.Integer, db.ForeignKey("humans.id"))
|
|
@@ -72,6 +73,7 @@ class Game(db.Model):
|
|
|
72
73
|
home_ot_score = db.Column(db.Integer, default=0)
|
|
73
74
|
visitor_ot_score = db.Column(db.Integer, default=0)
|
|
74
75
|
game_type = db.Column(db.String(50))
|
|
76
|
+
live_time = db.Column(db.String(50), nullable=True) # e.g., "Period 1, 1:10 left" for live games
|
|
75
77
|
went_to_ot = db.Column(db.Boolean, default=False)
|
|
76
78
|
home_period_1_shots = db.Column(db.Integer)
|
|
77
79
|
home_period_2_shots = db.Column(db.Integer)
|
|
@@ -245,6 +247,7 @@ class Level(db.Model):
|
|
|
245
247
|
org_id = db.Column(db.Integer, db.ForeignKey("organizations.id"), nullable=False)
|
|
246
248
|
skill_value = db.Column(db.Float) # A number from 0 (NHL) to 100 (pedestrian)
|
|
247
249
|
level_name = db.Column(db.String(100))
|
|
250
|
+
short_name = db.Column(db.String(50)) # Shortened display name (e.g., "D-7B-W" for "Adult Division 7B West")
|
|
248
251
|
level_alternative_name = db.Column(db.String(100))
|
|
249
252
|
is_seed = db.Column(db.Boolean, nullable=True, default=False) # New field
|
|
250
253
|
skill_propagation_sequence = db.Column(db.Integer, nullable=True, default=-1)
|
|
@@ -253,6 +256,17 @@ class Level(db.Model):
|
|
|
253
256
|
)
|
|
254
257
|
|
|
255
258
|
|
|
259
|
+
class Location(db.Model):
|
|
260
|
+
__tablename__ = "locations"
|
|
261
|
+
id = db.Column(db.Integer, primary_key=True)
|
|
262
|
+
location_in_game_source = db.Column(db.String(200), nullable=False, unique=True) # Raw string from Game.location, e.g., "San Jose Orange (N)"
|
|
263
|
+
location_name = db.Column(db.String(200), nullable=True) # Optional: Facility name, e.g., "Sharks Ice At San Jose"
|
|
264
|
+
rink_name = db.Column(db.String(200), nullable=True) # Optional: Specific rink, e.g., "Orange (N)"
|
|
265
|
+
address = db.Column(db.String(500), nullable=True)
|
|
266
|
+
google_maps_link = db.Column(db.String(500), nullable=True)
|
|
267
|
+
master_location_id = db.Column(db.Integer, db.ForeignKey("locations.id"), nullable=True) # Points to the canonical location for this rink
|
|
268
|
+
|
|
269
|
+
|
|
256
270
|
class LevelsMonthly(db.Model):
|
|
257
271
|
__tablename__ = "levels_monthly"
|
|
258
272
|
id = db.Column(db.Integer, primary_key=True)
|
|
@@ -427,6 +441,7 @@ class Season(db.Model):
|
|
|
427
441
|
id = db.Column(db.Integer, primary_key=True)
|
|
428
442
|
season_number = db.Column(db.Integer)
|
|
429
443
|
season_name = db.Column(db.String(100))
|
|
444
|
+
base_season_name = db.Column(db.String(100)) # Static prefix for season name (e.g., "Silver Stick", "Over", etc.)
|
|
430
445
|
start_date = db.Column(db.Date)
|
|
431
446
|
end_date = db.Column(db.Date)
|
|
432
447
|
league_number = db.Column(
|
|
@@ -4,7 +4,13 @@ from sqlalchemy.orm import synonym
|
|
|
4
4
|
from hockey_blast_common_lib.models import db
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
class
|
|
7
|
+
class AggregationTimestampMixin:
|
|
8
|
+
"""Mixin to add aggregation timestamp tracking to all stats models."""
|
|
9
|
+
aggregation_started_at = db.Column(db.DateTime, nullable=True)
|
|
10
|
+
aggregation_completed_at = db.Column(db.DateTime, nullable=True)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BaseStatsHuman(AggregationTimestampMixin, db.Model):
|
|
8
14
|
__abstract__ = True
|
|
9
15
|
id = db.Column(db.Integer, primary_key=True)
|
|
10
16
|
human_id = db.Column(db.Integer, db.ForeignKey("humans.id"), nullable=False)
|
|
@@ -92,7 +98,7 @@ class BaseStatsHuman(db.Model):
|
|
|
92
98
|
)
|
|
93
99
|
|
|
94
100
|
|
|
95
|
-
class BaseStatsSkater(db.Model):
|
|
101
|
+
class BaseStatsSkater(AggregationTimestampMixin, db.Model):
|
|
96
102
|
__abstract__ = True
|
|
97
103
|
id = db.Column(db.Integer, primary_key=True)
|
|
98
104
|
human_id = db.Column(db.Integer, db.ForeignKey("humans.id"), nullable=False)
|
|
@@ -193,7 +199,7 @@ class BaseStatsSkater(db.Model):
|
|
|
193
199
|
)
|
|
194
200
|
|
|
195
201
|
|
|
196
|
-
class BaseStatsGoalie(db.Model):
|
|
202
|
+
class BaseStatsGoalie(AggregationTimestampMixin, db.Model):
|
|
197
203
|
__abstract__ = True
|
|
198
204
|
id = db.Column(db.Integer, primary_key=True)
|
|
199
205
|
human_id = db.Column(db.Integer, db.ForeignKey("humans.id"), nullable=False)
|
|
@@ -261,7 +267,7 @@ class BaseStatsGoalie(db.Model):
|
|
|
261
267
|
)
|
|
262
268
|
|
|
263
269
|
|
|
264
|
-
class BaseStatsReferee(db.Model):
|
|
270
|
+
class BaseStatsReferee(AggregationTimestampMixin, db.Model):
|
|
265
271
|
__abstract__ = True
|
|
266
272
|
id = db.Column(db.Integer, primary_key=True)
|
|
267
273
|
human_id = db.Column(db.Integer, db.ForeignKey("humans.id"), nullable=False)
|
|
@@ -331,7 +337,7 @@ class BaseStatsReferee(db.Model):
|
|
|
331
337
|
)
|
|
332
338
|
|
|
333
339
|
|
|
334
|
-
class BaseStatsScorekeeper(db.Model):
|
|
340
|
+
class BaseStatsScorekeeper(AggregationTimestampMixin, db.Model):
|
|
335
341
|
__abstract__ = True
|
|
336
342
|
id = db.Column(db.Integer, primary_key=True)
|
|
337
343
|
human_id = db.Column(db.Integer, db.ForeignKey("humans.id"), nullable=False)
|
|
@@ -1045,3 +1051,80 @@ class DivisionStatsGoalieTeam(BaseStatsGoalie):
|
|
|
1045
1051
|
db.Index("idx_division_team_goalie_save_pct", "division_id", "save_percentage"),
|
|
1046
1052
|
db.Index("idx_division_team_goalie_gaa", "division_id", "goals_allowed_per_game"),
|
|
1047
1053
|
)
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
# Per-Game Statistics Models (for RAG system)
|
|
1057
|
+
# These models store individual game performance data for each player/goalie
|
|
1058
|
+
# CRITICAL: Only non-zero rows are saved (games where player recorded stats)
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
class GameStatsSkater(db.Model):
|
|
1062
|
+
"""Per-game skater statistics.
|
|
1063
|
+
|
|
1064
|
+
Stores individual game performance for skaters.
|
|
1065
|
+
Only records where player had non-zero stats are saved.
|
|
1066
|
+
Optimized for queries like "show me top N games by points for player X".
|
|
1067
|
+
"""
|
|
1068
|
+
__tablename__ = "game_stats_skater"
|
|
1069
|
+
|
|
1070
|
+
id = db.Column(db.Integer, primary_key=True)
|
|
1071
|
+
game_id = db.Column(db.Integer, db.ForeignKey("games.id"), nullable=False, index=True)
|
|
1072
|
+
human_id = db.Column(db.Integer, db.ForeignKey("humans.id"), nullable=False, index=True)
|
|
1073
|
+
team_id = db.Column(db.Integer, db.ForeignKey("teams.id"), nullable=False, index=True)
|
|
1074
|
+
org_id = db.Column(db.Integer, db.ForeignKey("organizations.id"), nullable=False, index=True)
|
|
1075
|
+
level_id = db.Column(db.Integer, db.ForeignKey("levels.id"), nullable=False, index=True)
|
|
1076
|
+
|
|
1077
|
+
# Denormalized game metadata for sorting/filtering
|
|
1078
|
+
game_date = db.Column(db.Date, nullable=False, index=True)
|
|
1079
|
+
game_time = db.Column(db.Time, nullable=False)
|
|
1080
|
+
|
|
1081
|
+
# Performance stats
|
|
1082
|
+
goals = db.Column(db.Integer, default=0, nullable=False)
|
|
1083
|
+
assists = db.Column(db.Integer, default=0, nullable=False)
|
|
1084
|
+
points = db.Column(db.Integer, default=0, nullable=False)
|
|
1085
|
+
penalty_minutes = db.Column(db.Integer, default=0, nullable=False)
|
|
1086
|
+
|
|
1087
|
+
# Tracking
|
|
1088
|
+
created_at = db.Column(db.DateTime, nullable=False, default=db.func.current_timestamp())
|
|
1089
|
+
|
|
1090
|
+
__table_args__ = (
|
|
1091
|
+
db.UniqueConstraint("game_id", "human_id", name="_game_human_uc_skater"),
|
|
1092
|
+
db.Index("idx_game_stats_skater_human_date", "human_id", "game_date", postgresql_using="btree"),
|
|
1093
|
+
db.Index("idx_game_stats_skater_human_team_date", "human_id", "team_id", "game_date", postgresql_using="btree"),
|
|
1094
|
+
)
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
class GameStatsGoalie(db.Model):
|
|
1098
|
+
"""Per-game goalie statistics.
|
|
1099
|
+
|
|
1100
|
+
Stores individual game performance for goalies.
|
|
1101
|
+
Only records where goalie faced shots are saved.
|
|
1102
|
+
Optimized for queries like "show me top N games by save% for goalie X".
|
|
1103
|
+
"""
|
|
1104
|
+
__tablename__ = "game_stats_goalie"
|
|
1105
|
+
|
|
1106
|
+
id = db.Column(db.Integer, primary_key=True)
|
|
1107
|
+
game_id = db.Column(db.Integer, db.ForeignKey("games.id"), nullable=False, index=True)
|
|
1108
|
+
human_id = db.Column(db.Integer, db.ForeignKey("humans.id"), nullable=False, index=True)
|
|
1109
|
+
team_id = db.Column(db.Integer, db.ForeignKey("teams.id"), nullable=False, index=True)
|
|
1110
|
+
org_id = db.Column(db.Integer, db.ForeignKey("organizations.id"), nullable=False, index=True)
|
|
1111
|
+
level_id = db.Column(db.Integer, db.ForeignKey("levels.id"), nullable=False, index=True)
|
|
1112
|
+
|
|
1113
|
+
# Denormalized game metadata for sorting/filtering
|
|
1114
|
+
game_date = db.Column(db.Date, nullable=False, index=True)
|
|
1115
|
+
game_time = db.Column(db.Time, nullable=False)
|
|
1116
|
+
|
|
1117
|
+
# Performance stats
|
|
1118
|
+
goals_allowed = db.Column(db.Integer, default=0, nullable=False)
|
|
1119
|
+
shots_faced = db.Column(db.Integer, default=0, nullable=False)
|
|
1120
|
+
saves = db.Column(db.Integer, default=0, nullable=False) # Computed: shots_faced - goals_allowed
|
|
1121
|
+
save_percentage = db.Column(db.Float, default=0.0, nullable=False) # Computed: saves / shots_faced
|
|
1122
|
+
|
|
1123
|
+
# Tracking
|
|
1124
|
+
created_at = db.Column(db.DateTime, nullable=False, default=db.func.current_timestamp())
|
|
1125
|
+
|
|
1126
|
+
__table_args__ = (
|
|
1127
|
+
db.UniqueConstraint("game_id", "human_id", "team_id", name="_game_human_uc_goalie"),
|
|
1128
|
+
db.Index("idx_game_stats_goalie_human_date", "human_id", "game_date", postgresql_using="btree"),
|
|
1129
|
+
db.Index("idx_game_stats_goalie_human_team_date", "human_id", "team_id", "game_date", postgresql_using="btree"),
|
|
1130
|
+
)
|
hockey_blast_common_lib/utils.py
CHANGED
|
@@ -269,7 +269,8 @@ def calculate_percentile_value(values, percentile):
|
|
|
269
269
|
lower_value = sorted_values[lower_index]
|
|
270
270
|
upper_value = sorted_values[upper_index]
|
|
271
271
|
|
|
272
|
-
|
|
272
|
+
# Convert Decimal to float to avoid type errors with database Decimal fields
|
|
273
|
+
return float(lower_value) + fraction * (float(upper_value) - float(lower_value))
|
|
273
274
|
|
|
274
275
|
|
|
275
276
|
# TEST DB CONNECTION, PERMISSIONS...
|
{hockey_blast_common_lib-0.1.66.dist-info → hockey_blast_common_lib-0.1.68.dist-info}/RECORD
RENAMED
|
@@ -1,31 +1,34 @@
|
|
|
1
1
|
hockey_blast_common_lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
hockey_blast_common_lib/aggregate_all_stats.py,sha256=lWDhdYMYFEdNFTM3FmAKWiHFYSkb0OLjTkagguHlwls,1914
|
|
3
|
-
hockey_blast_common_lib/
|
|
3
|
+
hockey_blast_common_lib/aggregate_game_stats_all.py,sha256=r0s2ki8y60pHL3wI8yDhRkAfNRRRgnOfzbJUwO6T4QY,4738
|
|
4
|
+
hockey_blast_common_lib/aggregate_game_stats_goalie.py,sha256=XMGhkCfj9EQ2kK1V8z0xLrCDi9t1Sqq0OxusG1uSXNk,9505
|
|
5
|
+
hockey_blast_common_lib/aggregate_game_stats_skater.py,sha256=00x8TUzPxClmfGZ7M6dPKUNJdYaCF1qIA6KmO7izI0Q,13013
|
|
6
|
+
hockey_blast_common_lib/aggregate_goalie_stats.py,sha256=hYhoCGf3p0lsLMYT11e-jqFLi3Yv2O3JmljdWRqww4I,20310
|
|
4
7
|
hockey_blast_common_lib/aggregate_h2h_stats.py,sha256=nStyIm_be25pKDYbPCaOSHFTjbaMLFxFAa2mTU1tL_k,11486
|
|
5
|
-
hockey_blast_common_lib/aggregate_human_stats.py,sha256=
|
|
6
|
-
hockey_blast_common_lib/aggregate_referee_stats.py,sha256=
|
|
8
|
+
hockey_blast_common_lib/aggregate_human_stats.py,sha256=2NrdgKHQnaYHaM9tNjq8512Ea9RuySTRdzpRlHpkt7k,29741
|
|
9
|
+
hockey_blast_common_lib/aggregate_referee_stats.py,sha256=UUbd_YqdOFGFCRMcobU5ROfojKXKtcpEYJ8pL8Wqka8,20049
|
|
7
10
|
hockey_blast_common_lib/aggregate_s2s_stats.py,sha256=gB3Oi1emtBWL3bKojUhHH01gAbQTSLvgqO1WcvLI6F8,7449
|
|
8
|
-
hockey_blast_common_lib/aggregate_scorekeeper_stats.py,sha256=
|
|
9
|
-
hockey_blast_common_lib/aggregate_skater_stats.py,sha256=
|
|
10
|
-
hockey_blast_common_lib/aggregate_team_goalie_stats.py,sha256=
|
|
11
|
-
hockey_blast_common_lib/aggregate_team_skater_stats.py,sha256=
|
|
11
|
+
hockey_blast_common_lib/aggregate_scorekeeper_stats.py,sha256=Bk6XOlv61kd1PwmWws8JEdsJt1nXnGEuDM1ecPaHEZM,18299
|
|
12
|
+
hockey_blast_common_lib/aggregate_skater_stats.py,sha256=u5a4i6FOOhkFGTAiJjn4_tGnJX2pFb9Qg1fNhpCt-xY,39016
|
|
13
|
+
hockey_blast_common_lib/aggregate_team_goalie_stats.py,sha256=ApgaR2beGmrV7docvDjXIGEACTt8f5T5pcmKisqLHpI,10044
|
|
14
|
+
hockey_blast_common_lib/aggregate_team_skater_stats.py,sha256=f93Dq884U47nlatWkyl4Tgtl-SvaBMg3pmR-rOJdWXU,12437
|
|
12
15
|
hockey_blast_common_lib/assign_skater_skill.py,sha256=it3jiSyUq7XpKqxzs88lyB5t1c3t1idIS_JRwq_FQoo,2810
|
|
13
16
|
hockey_blast_common_lib/db_connection.py,sha256=KACyHaOMeTX9zPNztYy8uOeB1ubIUenZcEKAeD5gC24,3333
|
|
14
17
|
hockey_blast_common_lib/dump_sample_db.sh,sha256=MY3lnzTXBoWd76-ZlZr9nWsKMEVgyRsUn-LZ2d1JWZs,810
|
|
15
18
|
hockey_blast_common_lib/embedding_utils.py,sha256=XbJvJlq6BKE6_oLzhUKcCrx6-TM8P-xl-S1SVLr_teU,10222
|
|
16
19
|
hockey_blast_common_lib/h2h_models.py,sha256=DEmQnmuacBVRNWvpRvq2RlwmhQYrT7XPOSTDNVtchr0,8597
|
|
17
|
-
hockey_blast_common_lib/hockey_blast_sample_backup.sql.gz,sha256=
|
|
18
|
-
hockey_blast_common_lib/models.py,sha256=
|
|
20
|
+
hockey_blast_common_lib/hockey_blast_sample_backup.sql.gz,sha256=DwPmAO5wg7a-BMw0qrVc8SJA0D8sTt5U5G0IEkSXh7k,4648840
|
|
21
|
+
hockey_blast_common_lib/models.py,sha256=HdSrKvNVvKNr2klvAinpj5aXiFxv9Vb0IrYkrIURj0o,22889
|
|
19
22
|
hockey_blast_common_lib/options.py,sha256=wzfGWKK_dHBA_PfiOvbP_-HtdoJCR0E7DkA5_cYDb_k,1578
|
|
20
23
|
hockey_blast_common_lib/progress_utils.py,sha256=7Txjpx5G4vHbnPTvNYuBA_WtrY0QFA4mDEYUDuZyY1E,3923
|
|
21
24
|
hockey_blast_common_lib/restore_sample_db.sh,sha256=7W3lzRZeu9zXIu1Bvtnaw8EHc1ulHmFM4mMh86oUQJo,2205
|
|
22
25
|
hockey_blast_common_lib/skills_in_divisions.py,sha256=9sGtU6SLj8BXb5R74ue1oPWa2nbk4JfJz5VmcuxetzA,8542
|
|
23
26
|
hockey_blast_common_lib/skills_propagation.py,sha256=qBK84nzkn8ZQHum0bdxFQwLvdgVE7DtWoPP9cdbOmRo,20201
|
|
24
|
-
hockey_blast_common_lib/stats_models.py,sha256=
|
|
27
|
+
hockey_blast_common_lib/stats_models.py,sha256=r-7cqStcIAO72gotRp1o8-HB4tXvJPsc4d3702gIQYM,39605
|
|
25
28
|
hockey_blast_common_lib/stats_utils.py,sha256=PTZvykl1zfEcojnzDFa1J3V3F5gREmoFG1lQHLnYHgo,300
|
|
26
|
-
hockey_blast_common_lib/utils.py,sha256=
|
|
29
|
+
hockey_blast_common_lib/utils.py,sha256=911NlMLzwMX5uwmytcpxNPRP-Y8OjDxTGKKIcyED5ls,9099
|
|
27
30
|
hockey_blast_common_lib/wsgi.py,sha256=oL9lPWccKLTAYIKPJkKZV5keVE-Dgosv74CBi770NNc,786
|
|
28
|
-
hockey_blast_common_lib-0.1.
|
|
29
|
-
hockey_blast_common_lib-0.1.
|
|
30
|
-
hockey_blast_common_lib-0.1.
|
|
31
|
-
hockey_blast_common_lib-0.1.
|
|
31
|
+
hockey_blast_common_lib-0.1.68.dist-info/METADATA,sha256=ZLJPHKBMKWh9U2WPCKNWjoRTYEgcmsR5JBHKYMHkEGY,318
|
|
32
|
+
hockey_blast_common_lib-0.1.68.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
33
|
+
hockey_blast_common_lib-0.1.68.dist-info/top_level.txt,sha256=wIR4LIkE40npoA2QlOdfCYlgFeGbsHR8Z6r0h46Vtgc,24
|
|
34
|
+
hockey_blast_common_lib-0.1.68.dist-info/RECORD,,
|
|
File without changes
|
{hockey_blast_common_lib-0.1.66.dist-info → hockey_blast_common_lib-0.1.68.dist-info}/top_level.txt
RENAMED
|
File without changes
|