hockey-blast-common-lib 0.1.64__py3-none-any.whl → 0.1.66__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.
@@ -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")
@@ -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,
@@ -243,6 +306,11 @@ def aggregate_goalie_stats(
243
306
  assign_ranks(stats_dict, "goals_allowed_per_game", reverse_rank=True)
244
307
  assign_ranks(stats_dict, "save_percentage")
245
308
 
309
+ # Calculate and insert percentile marker records
310
+ insert_percentile_markers_goalie(
311
+ session, stats_dict, aggregation_id, total_in_rank, StatsModel
312
+ )
313
+
246
314
  # Debug output for specific human
247
315
  if debug_human_id:
248
316
  if any(key[1] == debug_human_id for key in stats_dict):
@@ -29,8 +29,10 @@ from hockey_blast_common_lib.stats_models import (
29
29
  from hockey_blast_common_lib.stats_utils import ALL_ORGS_ID
30
30
  from hockey_blast_common_lib.utils import (
31
31
  assign_ranks,
32
+ calculate_percentile_value,
32
33
  get_all_division_ids_for_org,
33
34
  get_non_human_ids,
35
+ get_percentile_human,
34
36
  get_start_datetime,
35
37
  )
36
38
 
@@ -41,6 +43,62 @@ FORFEIT_STATUS = "FORFEIT"
41
43
  NOEVENTS_STATUS = "NOEVENTS"
42
44
 
43
45
 
46
+ def insert_percentile_markers_referee(
47
+ session, stats_dict, aggregation_id, total_in_rank, StatsModel
48
+ ):
49
+ """Insert percentile marker records for referee stats."""
50
+ if not stats_dict:
51
+ return
52
+
53
+ stat_fields = [
54
+ "games_reffed",
55
+ "games_participated",
56
+ "games_with_stats",
57
+ "penalties_given",
58
+ "penalties_per_game",
59
+ "gm_given",
60
+ "gm_per_game",
61
+ ]
62
+
63
+ percentiles = [25, 50, 75, 90, 95]
64
+
65
+ for percentile in percentiles:
66
+ percentile_human_id = get_percentile_human(session, "Ref", percentile)
67
+
68
+ percentile_values = {}
69
+ for field in stat_fields:
70
+ values = [stat[field] for stat in stats_dict.values() if field in stat]
71
+ if values:
72
+ percentile_values[field] = calculate_percentile_value(values, percentile)
73
+ else:
74
+ percentile_values[field] = 0
75
+
76
+ referee_stat = StatsModel(
77
+ aggregation_id=aggregation_id,
78
+ human_id=percentile_human_id,
79
+ games_reffed=int(percentile_values.get("games_reffed", 0)),
80
+ games_participated=int(percentile_values.get("games_participated", 0)),
81
+ games_participated_rank=0,
82
+ games_with_stats=int(percentile_values.get("games_with_stats", 0)),
83
+ games_with_stats_rank=0,
84
+ penalties_given=int(percentile_values.get("penalties_given", 0)),
85
+ penalties_per_game=percentile_values.get("penalties_per_game", 0.0),
86
+ gm_given=int(percentile_values.get("gm_given", 0)),
87
+ gm_per_game=percentile_values.get("gm_per_game", 0.0),
88
+ games_reffed_rank=0,
89
+ penalties_given_rank=0,
90
+ penalties_per_game_rank=0,
91
+ gm_given_rank=0,
92
+ gm_per_game_rank=0,
93
+ total_in_rank=total_in_rank,
94
+ first_game_id=None,
95
+ last_game_id=None,
96
+ )
97
+ session.add(referee_stat)
98
+
99
+ session.commit()
100
+
101
+
44
102
  def aggregate_referee_stats(
45
103
  session, aggregation_type, aggregation_id, aggregation_window=None
46
104
  ):
@@ -281,6 +339,11 @@ def aggregate_referee_stats(
281
339
  assign_ranks(stats_dict, "gm_given")
282
340
  assign_ranks(stats_dict, "gm_per_game")
283
341
 
342
+ # Calculate and insert percentile marker records
343
+ insert_percentile_markers_referee(
344
+ session, stats_dict, aggregation_id, total_in_rank, StatsModel
345
+ )
346
+
284
347
  # Insert aggregated stats into the appropriate table with progress output
285
348
  total_items = len(stats_dict)
286
349
  batch_size = 1000
@@ -22,7 +22,9 @@ from hockey_blast_common_lib.stats_models import (
22
22
  from hockey_blast_common_lib.stats_utils import ALL_ORGS_ID
23
23
  from hockey_blast_common_lib.utils import (
24
24
  assign_ranks,
25
+ calculate_percentile_value,
25
26
  get_non_human_ids,
27
+ get_percentile_human,
26
28
  get_start_datetime,
27
29
  )
28
30
 
@@ -33,6 +35,77 @@ FORFEIT_STATUS = "FORFEIT"
33
35
  NOEVENTS_STATUS = "NOEVENTS"
34
36
 
35
37
 
38
+ def insert_percentile_markers_scorekeeper(
39
+ session, stats_dict, aggregation_id, total_in_rank, StatsModel
40
+ ):
41
+ """Insert percentile marker records for scorekeeper stats."""
42
+ if not stats_dict:
43
+ return
44
+
45
+ stat_fields = [
46
+ "games_recorded",
47
+ "games_participated",
48
+ "games_with_stats",
49
+ "sog_given",
50
+ "sog_per_game",
51
+ "total_saves_recorded",
52
+ "avg_saves_per_game",
53
+ "avg_max_saves_per_5sec",
54
+ "avg_max_saves_per_20sec",
55
+ "peak_max_saves_per_5sec",
56
+ "peak_max_saves_per_20sec",
57
+ "quality_score",
58
+ ]
59
+
60
+ percentiles = [25, 50, 75, 90, 95]
61
+
62
+ for percentile in percentiles:
63
+ percentile_human_id = get_percentile_human(session, "Scorekeeper", percentile)
64
+
65
+ percentile_values = {}
66
+ for field in stat_fields:
67
+ values = [stat[field] for stat in stats_dict.values() if field in stat]
68
+ if values:
69
+ percentile_values[field] = calculate_percentile_value(values, percentile)
70
+ else:
71
+ percentile_values[field] = 0
72
+
73
+ scorekeeper_stat = StatsModel(
74
+ aggregation_id=aggregation_id,
75
+ human_id=percentile_human_id,
76
+ games_recorded=int(percentile_values.get("games_recorded", 0)),
77
+ games_participated=int(percentile_values.get("games_participated", 0)),
78
+ games_participated_rank=0,
79
+ games_with_stats=int(percentile_values.get("games_with_stats", 0)),
80
+ games_with_stats_rank=0,
81
+ sog_given=int(percentile_values.get("sog_given", 0)),
82
+ sog_per_game=percentile_values.get("sog_per_game", 0.0),
83
+ total_saves_recorded=int(percentile_values.get("total_saves_recorded", 0)),
84
+ avg_saves_per_game=percentile_values.get("avg_saves_per_game", 0.0),
85
+ avg_max_saves_per_5sec=percentile_values.get("avg_max_saves_per_5sec", 0.0),
86
+ avg_max_saves_per_20sec=percentile_values.get("avg_max_saves_per_20sec", 0.0),
87
+ peak_max_saves_per_5sec=int(percentile_values.get("peak_max_saves_per_5sec", 0)),
88
+ peak_max_saves_per_20sec=int(percentile_values.get("peak_max_saves_per_20sec", 0)),
89
+ quality_score=percentile_values.get("quality_score", 0.0),
90
+ games_recorded_rank=0,
91
+ sog_given_rank=0,
92
+ sog_per_game_rank=0,
93
+ total_saves_recorded_rank=0,
94
+ avg_saves_per_game_rank=0,
95
+ avg_max_saves_per_5sec_rank=0,
96
+ avg_max_saves_per_20sec_rank=0,
97
+ peak_max_saves_per_5sec_rank=0,
98
+ peak_max_saves_per_20sec_rank=0,
99
+ quality_score_rank=0,
100
+ total_in_rank=total_in_rank,
101
+ first_game_id=None,
102
+ last_game_id=None,
103
+ )
104
+ session.add(scorekeeper_stat)
105
+
106
+ session.commit()
107
+
108
+
36
109
  def calculate_quality_score(
37
110
  avg_max_saves_5sec, avg_max_saves_20sec, peak_max_saves_5sec, peak_max_saves_20sec
38
111
  ):
@@ -252,6 +325,11 @@ def aggregate_scorekeeper_stats(
252
325
  stats_dict, "quality_score", reverse_rank=True
253
326
  ) # Lower is better (less problematic)
254
327
 
328
+ # Calculate and insert percentile marker records
329
+ insert_percentile_markers_scorekeeper(
330
+ session, stats_dict, aggregation_id, total_in_rank, StatsModel
331
+ )
332
+
255
333
  # Insert aggregated stats into the appropriate table with progress output
256
334
  batch_size = 1000
257
335
  for i, (key, stat) in enumerate(stats_dict.items(), 1):
@@ -37,8 +37,10 @@ from hockey_blast_common_lib.stats_models import (
37
37
  )
38
38
  from hockey_blast_common_lib.stats_utils import ALL_ORGS_ID
39
39
  from hockey_blast_common_lib.utils import (
40
+ calculate_percentile_value,
40
41
  get_all_division_ids_for_org,
41
42
  get_non_human_ids,
43
+ get_percentile_human,
42
44
  get_start_datetime,
43
45
  )
44
46
 
@@ -115,6 +117,105 @@ def calculate_current_point_streak(session, human_id, filter_condition):
115
117
  return current_streak, avg_points_during_streak
116
118
 
117
119
 
120
+ def insert_percentile_markers_skater(
121
+ session, stats_dict, aggregation_id, total_in_rank, StatsModel, aggregation_window
122
+ ):
123
+ """Insert percentile marker records for skater stats.
124
+
125
+ For each stat field, calculate the 25th, 50th, 75th, 90th, and 95th percentile values
126
+ and insert marker records with fake human IDs.
127
+ """
128
+ if not stats_dict:
129
+ return
130
+
131
+ # Define the stat fields we want to calculate percentiles for
132
+ # Each field has percentile calculated SEPARATELY
133
+ stat_fields = [
134
+ "games_played",
135
+ "games_participated",
136
+ "games_with_stats",
137
+ "goals",
138
+ "assists",
139
+ "points",
140
+ "penalties",
141
+ "gm_penalties",
142
+ "goals_per_game",
143
+ "assists_per_game",
144
+ "points_per_game",
145
+ "penalties_per_game",
146
+ "gm_penalties_per_game",
147
+ ]
148
+
149
+ # Add streak fields only for all-time stats
150
+ if aggregation_window is None:
151
+ stat_fields.extend(
152
+ ["current_point_streak", "current_point_streak_avg_points"]
153
+ )
154
+
155
+ # For each percentile (25, 50, 75, 90, 95)
156
+ percentiles = [25, 50, 75, 90, 95]
157
+
158
+ for percentile in percentiles:
159
+ # Get or create the percentile marker human
160
+ percentile_human_id = get_percentile_human(session, "Skater", percentile)
161
+
162
+ # Calculate percentile values for each stat field SEPARATELY
163
+ percentile_values = {}
164
+ for field in stat_fields:
165
+ # Extract all values for this field
166
+ values = [stat[field] for stat in stats_dict.values() if field in stat]
167
+ if values:
168
+ percentile_values[field] = calculate_percentile_value(values, percentile)
169
+ else:
170
+ percentile_values[field] = 0
171
+
172
+ # Create the stats record for this percentile marker
173
+ skater_stat = StatsModel(
174
+ aggregation_id=aggregation_id,
175
+ human_id=percentile_human_id,
176
+ games_played=int(percentile_values.get("games_played", 0)),
177
+ games_participated=int(percentile_values.get("games_participated", 0)),
178
+ games_participated_rank=0, # Percentile markers don't have ranks
179
+ games_with_stats=int(percentile_values.get("games_with_stats", 0)),
180
+ games_with_stats_rank=0,
181
+ goals=int(percentile_values.get("goals", 0)),
182
+ assists=int(percentile_values.get("assists", 0)),
183
+ points=int(percentile_values.get("points", 0)),
184
+ penalties=int(percentile_values.get("penalties", 0)),
185
+ gm_penalties=int(percentile_values.get("gm_penalties", 0)),
186
+ goals_per_game=percentile_values.get("goals_per_game", 0.0),
187
+ points_per_game=percentile_values.get("points_per_game", 0.0),
188
+ assists_per_game=percentile_values.get("assists_per_game", 0.0),
189
+ penalties_per_game=percentile_values.get("penalties_per_game", 0.0),
190
+ gm_penalties_per_game=percentile_values.get("gm_penalties_per_game", 0.0),
191
+ games_played_rank=0,
192
+ goals_rank=0,
193
+ assists_rank=0,
194
+ points_rank=0,
195
+ penalties_rank=0,
196
+ gm_penalties_rank=0,
197
+ goals_per_game_rank=0,
198
+ points_per_game_rank=0,
199
+ assists_per_game_rank=0,
200
+ penalties_per_game_rank=0,
201
+ gm_penalties_per_game_rank=0,
202
+ total_in_rank=total_in_rank,
203
+ current_point_streak=int(
204
+ percentile_values.get("current_point_streak", 0)
205
+ ),
206
+ current_point_streak_rank=0,
207
+ current_point_streak_avg_points=percentile_values.get(
208
+ "current_point_streak_avg_points", 0.0
209
+ ),
210
+ current_point_streak_avg_points_rank=0,
211
+ first_game_id=None, # Percentile markers don't have game references
212
+ last_game_id=None,
213
+ )
214
+ session.add(skater_stat)
215
+
216
+ session.commit()
217
+
218
+
118
219
  def aggregate_skater_stats(
119
220
  session,
120
221
  aggregation_type,
@@ -561,6 +662,11 @@ def aggregate_skater_stats(
561
662
  assign_ranks(stats_dict, "current_point_streak")
562
663
  assign_ranks(stats_dict, "current_point_streak_avg_points")
563
664
 
665
+ # Calculate and insert percentile marker records
666
+ insert_percentile_markers_skater(
667
+ session, stats_dict, aggregation_id, total_in_rank, StatsModel, aggregation_window
668
+ )
669
+
564
670
  # Debug output for specific human
565
671
  if debug_human_id:
566
672
  if any(key[1] == debug_human_id for key in stats_dict):
@@ -0,0 +1,251 @@
1
+ """
2
+ Aggregate goalie statistics by team.
3
+
4
+ This module aggregates goalie statistics for each team, counting only games
5
+ where the goalie was on that specific team (using GameRoster.team_id).
6
+
7
+ Key difference from regular aggregation:
8
+ - Aggregates by (aggregation_id, team_id, human_id) instead of just (aggregation_id, human_id)
9
+ - Filters to only games where GameRoster.team_id matches the target team
10
+ - Stores results in OrgStatsGoalieTeam / DivisionStatsGoalieTeam
11
+
12
+ """
13
+ import os
14
+ import sys
15
+
16
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
17
+
18
+ import sqlalchemy
19
+ from sqlalchemy import func
20
+
21
+ from hockey_blast_common_lib.db_connection import create_session
22
+ from hockey_blast_common_lib.models import (
23
+ Division,
24
+ Game,
25
+ GameRoster,
26
+ GoalieSaves,
27
+ Human,
28
+ Organization,
29
+ Team,
30
+ )
31
+ from hockey_blast_common_lib.options import (
32
+ MIN_GAMES_FOR_DIVISION_STATS,
33
+ MIN_GAMES_FOR_ORG_STATS,
34
+ )
35
+ from hockey_blast_common_lib.progress_utils import create_progress_tracker
36
+ from hockey_blast_common_lib.stats_models import (
37
+ DivisionStatsGoalieTeam,
38
+ OrgStatsGoalieTeam,
39
+ )
40
+ from hockey_blast_common_lib.utils import (
41
+ calculate_percentile_value,
42
+ get_non_human_ids,
43
+ get_percentile_human,
44
+ )
45
+
46
+ # Import status constants for game filtering
47
+ FINAL_STATUS = "Final"
48
+ FINAL_SO_STATUS = "Final(SO)"
49
+ FORFEIT_STATUS = "FORFEIT"
50
+ NOEVENTS_STATUS = "NOEVENTS"
51
+
52
+
53
+ def aggregate_team_goalie_stats(session, aggregation_type, aggregation_id):
54
+ """
55
+ Aggregate goalie stats by team for an organization or division.
56
+
57
+ For each team in the aggregation scope, calculates stats for all goalies
58
+ who played for that team, counting only games where they were on that team.
59
+
60
+ Args:
61
+ session: Database session
62
+ aggregation_type: "org" or "division"
63
+ aggregation_id: ID of the organization or division
64
+ """
65
+ human_ids_to_filter = get_non_human_ids(session)
66
+
67
+ # Determine aggregation details
68
+ if aggregation_type == "org":
69
+ StatsModel = OrgStatsGoalieTeam
70
+ min_games = MIN_GAMES_FOR_ORG_STATS
71
+ aggregation_name = (
72
+ session.query(Organization)
73
+ .filter(Organization.id == aggregation_id)
74
+ .first()
75
+ .organization_name
76
+ )
77
+ filter_condition = Game.org_id == aggregation_id
78
+ elif aggregation_type == "division":
79
+ StatsModel = DivisionStatsGoalieTeam
80
+ min_games = MIN_GAMES_FOR_DIVISION_STATS
81
+ aggregation_name = (
82
+ session.query(Division).filter(Division.id == aggregation_id).first().level
83
+ )
84
+ filter_condition = Game.division_id == aggregation_id
85
+ else:
86
+ raise ValueError(f"Invalid aggregation type: {aggregation_type}")
87
+
88
+ print(f"Aggregating team goalie stats for {aggregation_name}...")
89
+
90
+ # Delete existing stats for this aggregation
91
+ session.query(StatsModel).filter(StatsModel.aggregation_id == aggregation_id).delete()
92
+ session.commit()
93
+
94
+ # Get all teams in this aggregation scope
95
+ if aggregation_type == "org":
96
+ teams_query = (
97
+ session.query(Team.id, Team.name)
98
+ .join(Game, (Game.home_team_id == Team.id) | (Game.visitor_team_id == Team.id))
99
+ .filter(Game.org_id == aggregation_id)
100
+ .distinct()
101
+ )
102
+ else: # division
103
+ teams_query = (
104
+ session.query(Team.id, Team.name)
105
+ .join(Game, (Game.home_team_id == Team.id) | (Game.visitor_team_id == Team.id))
106
+ .filter(Game.division_id == aggregation_id)
107
+ .distinct()
108
+ )
109
+
110
+ teams = teams_query.all()
111
+ print(f"Found {len(teams)} teams in {aggregation_name}")
112
+
113
+ # Process each team
114
+ progress = create_progress_tracker(len(teams), description="Processing teams")
115
+ for team_id, team_name in teams:
116
+ progress.update(1)
117
+
118
+ # Aggregate stats for goalies on this team
119
+ # Filter to only games where goalies were on THIS team
120
+ games_played_query = (
121
+ session.query(
122
+ GameRoster.human_id,
123
+ func.count(Game.id).label("games_played"),
124
+ func.count(Game.id).label("games_participated"),
125
+ func.count(Game.id).label("games_with_stats"),
126
+ func.array_agg(Game.id).label("game_ids"),
127
+ )
128
+ .join(Game, Game.id == GameRoster.game_id)
129
+ .filter(
130
+ GameRoster.team_id == team_id, # KEY: Filter by team
131
+ GameRoster.role.ilike("g"), # Only goalies
132
+ GameRoster.human_id.notin_(human_ids_to_filter),
133
+ Game.status.in_([FINAL_STATUS, FINAL_SO_STATUS, FORFEIT_STATUS, NOEVENTS_STATUS]),
134
+ filter_condition, # org_id or division_id filter
135
+ )
136
+ .group_by(GameRoster.human_id)
137
+ .having(func.count(Game.id) >= min_games)
138
+ )
139
+
140
+ games_played_data = games_played_query.all()
141
+ if not games_played_data:
142
+ continue # No goalies met minimum games for this team
143
+
144
+ # Create stats dictionary
145
+ stats_dict = {}
146
+ for row in games_played_data:
147
+ stats_dict[row.human_id] = {
148
+ "games_played": row.games_played,
149
+ "games_participated": row.games_participated,
150
+ "games_with_stats": row.games_with_stats,
151
+ "game_ids": row.game_ids,
152
+ "first_game_id": row.game_ids[0] if row.game_ids else None,
153
+ "last_game_id": row.game_ids[-1] if row.game_ids else None,
154
+ }
155
+
156
+ # Aggregate goals allowed and shots faced from GoalieSaves table
157
+ goalie_saves_query = (
158
+ session.query(
159
+ GameRoster.human_id,
160
+ func.sum(GoalieSaves.goals_allowed).label("goals_allowed"),
161
+ func.sum(GoalieSaves.shots_against).label("shots_faced"),
162
+ )
163
+ .join(Game, Game.id == GameRoster.game_id)
164
+ .join(
165
+ GoalieSaves,
166
+ (GoalieSaves.game_id == Game.id) & (GoalieSaves.goalie_id == GameRoster.human_id),
167
+ )
168
+ .filter(
169
+ GameRoster.team_id == team_id, # KEY: Filter by team
170
+ GameRoster.role.ilike("g"),
171
+ GameRoster.human_id.in_(stats_dict.keys()),
172
+ Game.status.in_([FINAL_STATUS, FINAL_SO_STATUS]),
173
+ filter_condition,
174
+ )
175
+ .group_by(GameRoster.human_id)
176
+ )
177
+
178
+ for row in goalie_saves_query.all():
179
+ if row.human_id in stats_dict:
180
+ stats_dict[row.human_id]["goals_allowed"] = row.goals_allowed or 0
181
+ stats_dict[row.human_id]["shots_faced"] = row.shots_faced or 0
182
+
183
+ # Calculate per-game averages and save percentage
184
+ for human_id, stats in stats_dict.items():
185
+ games_with_stats = stats.get("games_with_stats", 0)
186
+ goals_allowed = stats.get("goals_allowed", 0)
187
+ shots_faced = stats.get("shots_faced", 0)
188
+
189
+ if games_with_stats > 0:
190
+ stats["goals_allowed_per_game"] = goals_allowed / games_with_stats
191
+ else:
192
+ stats["goals_allowed_per_game"] = 0.0
193
+
194
+ if shots_faced > 0:
195
+ saves = shots_faced - goals_allowed
196
+ stats["save_percentage"] = saves / shots_faced
197
+ else:
198
+ stats["save_percentage"] = 0.0
199
+
200
+ # Insert stats for each goalie on this team
201
+ for human_id, stats in stats_dict.items():
202
+ goalie_stat = StatsModel(
203
+ aggregation_id=aggregation_id,
204
+ team_id=team_id,
205
+ human_id=human_id,
206
+ games_played=stats.get("games_played", 0),
207
+ games_participated=stats.get("games_participated", 0),
208
+ games_with_stats=stats.get("games_with_stats", 0),
209
+ goals_allowed=stats.get("goals_allowed", 0),
210
+ shots_faced=stats.get("shots_faced", 0),
211
+ goals_allowed_per_game=stats.get("goals_allowed_per_game", 0.0),
212
+ save_percentage=stats.get("save_percentage", 0.0),
213
+ total_in_rank=len(stats_dict),
214
+ first_game_id=stats.get("first_game_id"),
215
+ last_game_id=stats.get("last_game_id"),
216
+ # Initialize rank fields to 0 (not calculated for team stats)
217
+ games_played_rank=0,
218
+ games_participated_rank=0,
219
+ games_with_stats_rank=0,
220
+ goals_allowed_rank=0,
221
+ shots_faced_rank=0,
222
+ goals_allowed_per_game_rank=0,
223
+ save_percentage_rank=0,
224
+ )
225
+ session.add(goalie_stat)
226
+
227
+ session.commit()
228
+ progress.finish()
229
+ print(f"✓ Team goalie stats aggregation complete for {aggregation_name}")
230
+
231
+
232
+ def run_aggregate_team_goalie_stats():
233
+ """
234
+ Run team goalie stats aggregation for all organizations and divisions.
235
+ """
236
+ from hockey_blast_common_lib.utils import get_all_division_ids_for_org
237
+
238
+ session = create_session("boss")
239
+
240
+ # Get all org_id present in the Organization table
241
+ org_ids = session.query(Organization.id).all()
242
+ org_ids = [org_id[0] for org_id in org_ids]
243
+
244
+ for org_id in org_ids:
245
+ # Aggregate for organization level
246
+ aggregate_team_goalie_stats(session, "org", org_id)
247
+
248
+ # Aggregate for all divisions in this organization
249
+ division_ids = get_all_division_ids_for_org(session, org_id)
250
+ for division_id in division_ids:
251
+ aggregate_team_goalie_stats(session, "division", division_id)
@@ -0,0 +1,299 @@
1
+ """
2
+ Aggregate skater statistics by team.
3
+
4
+ This module aggregates player statistics for each team, counting only games
5
+ where the player was on that specific team (using GameRoster.team_id).
6
+
7
+ Key difference from regular aggregation:
8
+ - Aggregates by (aggregation_id, team_id, human_id) instead of just (aggregation_id, human_id)
9
+ - Filters to only games where GameRoster.team_id matches the target team
10
+ - Stores results in OrgStatsSkaterTeam / DivisionStatsSkaterTeam
11
+
12
+ """
13
+ import os
14
+ import sys
15
+
16
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
17
+
18
+ import sqlalchemy
19
+ from sqlalchemy import and_, case, func
20
+
21
+ from hockey_blast_common_lib.db_connection import create_session
22
+ from hockey_blast_common_lib.models import (
23
+ Division,
24
+ Game,
25
+ GameRoster,
26
+ Goal,
27
+ Human,
28
+ Organization,
29
+ Penalty,
30
+ Team,
31
+ )
32
+ from hockey_blast_common_lib.options import (
33
+ MIN_GAMES_FOR_DIVISION_STATS,
34
+ MIN_GAMES_FOR_ORG_STATS,
35
+ )
36
+ from hockey_blast_common_lib.progress_utils import create_progress_tracker
37
+ from hockey_blast_common_lib.stats_models import (
38
+ DivisionStatsSkaterTeam,
39
+ OrgStatsSkaterTeam,
40
+ )
41
+ from hockey_blast_common_lib.utils import (
42
+ calculate_percentile_value,
43
+ get_non_human_ids,
44
+ get_percentile_human,
45
+ )
46
+
47
+ # Import status constants for game filtering
48
+ FINAL_STATUS = "Final"
49
+ FINAL_SO_STATUS = "Final(SO)"
50
+ FORFEIT_STATUS = "FORFEIT"
51
+ NOEVENTS_STATUS = "NOEVENTS"
52
+
53
+
54
+ def aggregate_team_skater_stats(session, aggregation_type, aggregation_id):
55
+ """
56
+ Aggregate skater stats by team for an organization or division.
57
+
58
+ For each team in the aggregation scope, calculates stats for all players
59
+ who played for that team, counting only games where they were on that team.
60
+
61
+ Args:
62
+ session: Database session
63
+ aggregation_type: "org" or "division"
64
+ aggregation_id: ID of the organization or division
65
+ """
66
+ human_ids_to_filter = get_non_human_ids(session)
67
+
68
+ # Determine aggregation details
69
+ if aggregation_type == "org":
70
+ StatsModel = OrgStatsSkaterTeam
71
+ min_games = MIN_GAMES_FOR_ORG_STATS
72
+ aggregation_name = (
73
+ session.query(Organization)
74
+ .filter(Organization.id == aggregation_id)
75
+ .first()
76
+ .organization_name
77
+ )
78
+ filter_condition = Game.org_id == aggregation_id
79
+ elif aggregation_type == "division":
80
+ StatsModel = DivisionStatsSkaterTeam
81
+ min_games = MIN_GAMES_FOR_DIVISION_STATS
82
+ aggregation_name = (
83
+ session.query(Division).filter(Division.id == aggregation_id).first().level
84
+ )
85
+ filter_condition = Game.division_id == aggregation_id
86
+ else:
87
+ raise ValueError(f"Invalid aggregation type: {aggregation_type}")
88
+
89
+ print(f"Aggregating team skater stats for {aggregation_name}...")
90
+
91
+ # Delete existing stats for this aggregation
92
+ session.query(StatsModel).filter(StatsModel.aggregation_id == aggregation_id).delete()
93
+ session.commit()
94
+
95
+ # Get all teams in this aggregation scope
96
+ if aggregation_type == "org":
97
+ teams_query = (
98
+ session.query(Team.id, Team.name)
99
+ .join(Game, (Game.home_team_id == Team.id) | (Game.visitor_team_id == Team.id))
100
+ .filter(Game.org_id == aggregation_id)
101
+ .distinct()
102
+ )
103
+ else: # division
104
+ teams_query = (
105
+ session.query(Team.id, Team.name)
106
+ .join(Game, (Game.home_team_id == Team.id) | (Game.visitor_team_id == Team.id))
107
+ .filter(Game.division_id == aggregation_id)
108
+ .distinct()
109
+ )
110
+
111
+ teams = teams_query.all()
112
+ print(f"Found {len(teams)} teams in {aggregation_name}")
113
+
114
+ # Process each team
115
+ progress = create_progress_tracker(len(teams), description="Processing teams")
116
+ for team_id, team_name in teams:
117
+ progress.update(1)
118
+
119
+ # Aggregate stats for this team
120
+ # Filter to only games where players were on THIS team
121
+ games_played_query = (
122
+ session.query(
123
+ GameRoster.human_id,
124
+ func.count(Game.id).label("games_played"),
125
+ func.count(Game.id).label("games_participated"),
126
+ func.count(Game.id).label("games_with_stats"),
127
+ func.array_agg(Game.id).label("game_ids"),
128
+ )
129
+ .join(Game, Game.id == GameRoster.game_id)
130
+ .filter(
131
+ GameRoster.team_id == team_id, # KEY: Filter by team
132
+ ~GameRoster.role.ilike("g"), # Exclude goalies
133
+ GameRoster.human_id.notin_(human_ids_to_filter),
134
+ Game.status.in_([FINAL_STATUS, FINAL_SO_STATUS, FORFEIT_STATUS, NOEVENTS_STATUS]),
135
+ filter_condition, # org_id or division_id filter
136
+ )
137
+ .group_by(GameRoster.human_id)
138
+ .having(func.count(Game.id) >= min_games)
139
+ )
140
+
141
+ games_played_data = games_played_query.all()
142
+ if not games_played_data:
143
+ continue # No players met minimum games for this team
144
+
145
+ # Create stats dictionary
146
+ stats_dict = {}
147
+ for row in games_played_data:
148
+ stats_dict[row.human_id] = {
149
+ "games_played": row.games_played,
150
+ "games_participated": row.games_participated,
151
+ "games_with_stats": row.games_with_stats,
152
+ "game_ids": row.game_ids,
153
+ "first_game_id": row.game_ids[0] if row.game_ids else None,
154
+ "last_game_id": row.game_ids[-1] if row.game_ids else None,
155
+ }
156
+
157
+ # Aggregate goals, assists, points
158
+ goals_assists_query = (
159
+ session.query(
160
+ GameRoster.human_id,
161
+ func.count(func.distinct(case((Goal.goal_scorer_id == GameRoster.human_id, Goal.id)))).label("goals"),
162
+ func.count(
163
+ func.distinct(
164
+ case(
165
+ (
166
+ (Goal.assist_1_id == GameRoster.human_id) | (Goal.assist_2_id == GameRoster.human_id),
167
+ Goal.id,
168
+ )
169
+ )
170
+ )
171
+ ).label("assists"),
172
+ )
173
+ .join(Game, Game.id == GameRoster.game_id)
174
+ .outerjoin(Goal, Game.id == Goal.game_id)
175
+ .filter(
176
+ GameRoster.team_id == team_id, # KEY: Filter by team
177
+ ~GameRoster.role.ilike("g"),
178
+ GameRoster.human_id.in_(stats_dict.keys()),
179
+ Game.status.in_([FINAL_STATUS, FINAL_SO_STATUS]),
180
+ filter_condition,
181
+ )
182
+ .group_by(GameRoster.human_id)
183
+ )
184
+
185
+ for row in goals_assists_query.all():
186
+ if row.human_id in stats_dict:
187
+ stats_dict[row.human_id]["goals"] = row.goals
188
+ stats_dict[row.human_id]["assists"] = row.assists
189
+ stats_dict[row.human_id]["points"] = row.goals + row.assists
190
+
191
+ # Aggregate penalties
192
+ penalties_query = (
193
+ session.query(
194
+ GameRoster.human_id,
195
+ func.count(Penalty.id).label("penalties"),
196
+ func.sum(case((Penalty.penalty_minutes == "GM", 1), else_=0)).label("gm_penalties"),
197
+ )
198
+ .join(Game, Game.id == GameRoster.game_id)
199
+ .outerjoin(Penalty, and_(Game.id == Penalty.game_id, Penalty.penalized_player_id == GameRoster.human_id))
200
+ .filter(
201
+ GameRoster.team_id == team_id, # KEY: Filter by team
202
+ ~GameRoster.role.ilike("g"),
203
+ GameRoster.human_id.in_(stats_dict.keys()),
204
+ Game.status.in_([FINAL_STATUS, FINAL_SO_STATUS, FORFEIT_STATUS, NOEVENTS_STATUS]),
205
+ filter_condition,
206
+ )
207
+ .group_by(GameRoster.human_id)
208
+ )
209
+
210
+ for row in penalties_query.all():
211
+ if row.human_id in stats_dict:
212
+ stats_dict[row.human_id]["penalties"] = row.penalties
213
+ stats_dict[row.human_id]["gm_penalties"] = row.gm_penalties
214
+
215
+ # Calculate per-game averages
216
+ for human_id, stats in stats_dict.items():
217
+ games_with_stats = stats.get("games_with_stats", 0)
218
+ if games_with_stats > 0:
219
+ stats["goals_per_game"] = stats.get("goals", 0) / games_with_stats
220
+ stats["assists_per_game"] = stats.get("assists", 0) / games_with_stats
221
+ stats["points_per_game"] = stats.get("points", 0) / games_with_stats
222
+ stats["penalties_per_game"] = stats.get("penalties", 0) / games_with_stats
223
+ stats["gm_penalties_per_game"] = stats.get("gm_penalties", 0) / games_with_stats
224
+ else:
225
+ stats["goals_per_game"] = 0.0
226
+ stats["assists_per_game"] = 0.0
227
+ stats["points_per_game"] = 0.0
228
+ stats["penalties_per_game"] = 0.0
229
+ stats["gm_penalties_per_game"] = 0.0
230
+
231
+ # Insert stats for each player on this team
232
+ for human_id, stats in stats_dict.items():
233
+ skater_stat = StatsModel(
234
+ aggregation_id=aggregation_id,
235
+ team_id=team_id,
236
+ human_id=human_id,
237
+ games_played=stats.get("games_played", 0),
238
+ games_participated=stats.get("games_participated", 0),
239
+ games_with_stats=stats.get("games_with_stats", 0),
240
+ goals=stats.get("goals", 0),
241
+ assists=stats.get("assists", 0),
242
+ points=stats.get("points", 0),
243
+ penalties=stats.get("penalties", 0),
244
+ gm_penalties=stats.get("gm_penalties", 0),
245
+ goals_per_game=stats.get("goals_per_game", 0.0),
246
+ assists_per_game=stats.get("assists_per_game", 0.0),
247
+ points_per_game=stats.get("points_per_game", 0.0),
248
+ penalties_per_game=stats.get("penalties_per_game", 0.0),
249
+ gm_penalties_per_game=stats.get("gm_penalties_per_game", 0.0),
250
+ total_in_rank=len(stats_dict),
251
+ first_game_id=stats.get("first_game_id"),
252
+ last_game_id=stats.get("last_game_id"),
253
+ # Initialize streak fields to 0 (not calculated for team stats)
254
+ current_point_streak=0,
255
+ current_point_streak_avg_points=0.0,
256
+ # Ranks will be assigned later if needed
257
+ games_played_rank=0,
258
+ games_participated_rank=0,
259
+ games_with_stats_rank=0,
260
+ goals_rank=0,
261
+ assists_rank=0,
262
+ points_rank=0,
263
+ penalties_rank=0,
264
+ gm_penalties_rank=0,
265
+ goals_per_game_rank=0,
266
+ assists_per_game_rank=0,
267
+ points_per_game_rank=0,
268
+ penalties_per_game_rank=0,
269
+ gm_penalties_per_game_rank=0,
270
+ current_point_streak_rank=0,
271
+ current_point_streak_avg_points_rank=0,
272
+ )
273
+ session.add(skater_stat)
274
+
275
+ session.commit()
276
+ progress.finish()
277
+ print(f"✓ Team skater stats aggregation complete for {aggregation_name}")
278
+
279
+
280
+ def run_aggregate_team_skater_stats():
281
+ """
282
+ Run team skater stats aggregation for all organizations and divisions.
283
+ """
284
+ from hockey_blast_common_lib.utils import get_all_division_ids_for_org
285
+
286
+ session = create_session("boss")
287
+
288
+ # Get all org_id present in the Organization table
289
+ org_ids = session.query(Organization.id).all()
290
+ org_ids = [org_id[0] for org_id in org_ids]
291
+
292
+ for org_id in org_ids:
293
+ # Aggregate for organization level
294
+ aggregate_team_skater_stats(session, "org", org_id)
295
+
296
+ # Aggregate for all divisions in this organization
297
+ division_ids = get_all_division_ids_for_org(session, org_id)
298
+ for division_id in division_ids:
299
+ aggregate_team_skater_stats(session, "division", division_id)
@@ -922,3 +922,126 @@ class SkillValuePPGRatio(db.Model):
922
922
  "from_skill_value", "to_skill_value", name="_from_to_skill_value_uc"
923
923
  ),
924
924
  )
925
+
926
+
927
+ # Team-based statistics models (inherit from existing bases, add team_id field)
928
+ class OrgStatsSkaterTeam(BaseStatsSkater):
929
+ __tablename__ = "org_stats_skater_team"
930
+ org_id = db.Column(db.Integer, db.ForeignKey("organizations.id"), nullable=False)
931
+ team_id = db.Column(db.Integer, db.ForeignKey("teams.id"), nullable=False)
932
+ aggregation_id = synonym("org_id")
933
+
934
+ @declared_attr
935
+ def aggregation_type(cls):
936
+ return "org_team"
937
+
938
+ @classmethod
939
+ def get_aggregation_column(cls):
940
+ return "org_id"
941
+
942
+ @declared_attr
943
+ def __table_args__(cls):
944
+ return (
945
+ db.UniqueConstraint(
946
+ "org_id",
947
+ "team_id",
948
+ "human_id",
949
+ name="_org_team_human_uc_skater_team1",
950
+ ),
951
+ db.Index("idx_org_team_team_id", "team_id"),
952
+ db.Index("idx_org_team_human_id", "human_id"),
953
+ db.Index("idx_org_team_goals_per_game", "org_id", "goals_per_game"),
954
+ db.Index("idx_org_team_points_per_game", "org_id", "points_per_game"),
955
+ db.Index("idx_org_team_assists_per_game", "org_id", "assists_per_game"),
956
+ )
957
+
958
+
959
+ class DivisionStatsSkaterTeam(BaseStatsSkater):
960
+ __tablename__ = "division_stats_skater_team"
961
+ division_id = db.Column(db.Integer, db.ForeignKey("divisions.id"), nullable=False)
962
+ team_id = db.Column(db.Integer, db.ForeignKey("teams.id"), nullable=False)
963
+ aggregation_id = synonym("division_id")
964
+
965
+ @declared_attr
966
+ def aggregation_type(cls):
967
+ return "division_team"
968
+
969
+ @classmethod
970
+ def get_aggregation_column(cls):
971
+ return "division_id"
972
+
973
+ @declared_attr
974
+ def __table_args__(cls):
975
+ return (
976
+ db.UniqueConstraint(
977
+ "division_id",
978
+ "team_id",
979
+ "human_id",
980
+ name="_division_team_human_uc_skater_team1",
981
+ ),
982
+ db.Index("idx_division_team_team_id", "team_id"),
983
+ db.Index("idx_division_team_human_id", "human_id"),
984
+ db.Index("idx_division_team_goals_per_game", "division_id", "goals_per_game"),
985
+ db.Index("idx_division_team_points_per_game", "division_id", "points_per_game"),
986
+ db.Index("idx_division_team_assists_per_game", "division_id", "assists_per_game"),
987
+ )
988
+
989
+
990
+ class OrgStatsGoalieTeam(BaseStatsGoalie):
991
+ __tablename__ = "org_stats_goalie_team"
992
+ org_id = db.Column(db.Integer, db.ForeignKey("organizations.id"), nullable=False)
993
+ team_id = db.Column(db.Integer, db.ForeignKey("teams.id"), nullable=False)
994
+ aggregation_id = synonym("org_id")
995
+
996
+ @declared_attr
997
+ def aggregation_type(cls):
998
+ return "org_team"
999
+
1000
+ @classmethod
1001
+ def get_aggregation_column(cls):
1002
+ return "org_id"
1003
+
1004
+ @declared_attr
1005
+ def __table_args__(cls):
1006
+ return (
1007
+ db.UniqueConstraint(
1008
+ "org_id",
1009
+ "team_id",
1010
+ "human_id",
1011
+ name="_org_team_human_uc_goalie_team1",
1012
+ ),
1013
+ db.Index("idx_org_team_goalie_team_id", "team_id"),
1014
+ db.Index("idx_org_team_goalie_human_id", "human_id"),
1015
+ db.Index("idx_org_team_goalie_save_pct", "org_id", "save_percentage"),
1016
+ db.Index("idx_org_team_goalie_gaa", "org_id", "goals_allowed_per_game"),
1017
+ )
1018
+
1019
+
1020
+ class DivisionStatsGoalieTeam(BaseStatsGoalie):
1021
+ __tablename__ = "division_stats_goalie_team"
1022
+ division_id = db.Column(db.Integer, db.ForeignKey("divisions.id"), nullable=False)
1023
+ team_id = db.Column(db.Integer, db.ForeignKey("teams.id"), nullable=False)
1024
+ aggregation_id = synonym("division_id")
1025
+
1026
+ @declared_attr
1027
+ def aggregation_type(cls):
1028
+ return "division_team"
1029
+
1030
+ @classmethod
1031
+ def get_aggregation_column(cls):
1032
+ return "division_id"
1033
+
1034
+ @declared_attr
1035
+ def __table_args__(cls):
1036
+ return (
1037
+ db.UniqueConstraint(
1038
+ "division_id",
1039
+ "team_id",
1040
+ "human_id",
1041
+ name="_division_team_human_uc_goalie_team1",
1042
+ ),
1043
+ db.Index("idx_division_team_goalie_team_id", "team_id"),
1044
+ db.Index("idx_division_team_goalie_human_id", "human_id"),
1045
+ db.Index("idx_division_team_goalie_save_pct", "division_id", "save_percentage"),
1046
+ db.Index("idx_division_team_goalie_gaa", "division_id", "goals_allowed_per_game"),
1047
+ )
@@ -85,6 +85,7 @@ def get_non_human_ids(session):
85
85
 
86
86
  Returns set of human_ids that should be filtered out from statistics.
87
87
  Filters out placeholder names like "Home", "Away", "Unknown", etc.
88
+ Also excludes percentile marker humans.
88
89
  """
89
90
  not_human_names = [
90
91
  ("Home", None, None),
@@ -95,7 +96,11 @@ def get_non_human_ids(session):
95
96
  ("Unassigned", None, None),
96
97
  ("Not", "Signed", "In"),
97
98
  ("Incognito", None, None),
99
+ ("Empty", None , "Net"),
100
+ ("Fake", "Stats", "Human"),
101
+ (None, None, "Percentile"),
98
102
  ]
103
+
99
104
  return get_human_ids_by_names(session, not_human_names)
100
105
 
101
106
 
@@ -206,6 +211,67 @@ def get_fake_level(session):
206
211
  return fake_skill
207
212
 
208
213
 
214
+ def get_percentile_human(session, entity_type, percentile):
215
+ """Get or create a human record representing a percentile marker.
216
+
217
+ Args:
218
+ session: Database session
219
+ entity_type: One of "Skater", "Goalie", "Ref", "Scorekeeper"
220
+ percentile: One of 25, 50, 75, 90, 95
221
+
222
+ Returns:
223
+ human_id of the percentile marker record
224
+ """
225
+ first_name = entity_type
226
+ middle_name = str(percentile)
227
+ last_name = "Percentile"
228
+
229
+ # Check if the human already exists
230
+ existing_human = (
231
+ session.query(Human)
232
+ .filter_by(first_name=first_name, middle_name=middle_name, last_name=last_name)
233
+ .first()
234
+ )
235
+ if existing_human:
236
+ return existing_human.id
237
+
238
+ # Create a new human
239
+ human = Human(first_name=first_name, middle_name=middle_name, last_name=last_name)
240
+ session.add(human)
241
+ session.commit()
242
+
243
+ return human.id
244
+
245
+
246
+ def calculate_percentile_value(values, percentile):
247
+ """Calculate the percentile value from a list of values.
248
+
249
+ Args:
250
+ values: List of numeric values
251
+ percentile: Percentile to calculate (e.g., 25, 50, 75, 90, 95)
252
+
253
+ Returns:
254
+ The value at the given percentile
255
+ """
256
+ if not values:
257
+ return 0
258
+
259
+ sorted_values = sorted(values)
260
+ n = len(sorted_values)
261
+
262
+ # Calculate index (using linear interpolation method)
263
+ index = (percentile / 100.0) * (n - 1)
264
+ lower_index = int(index)
265
+ upper_index = min(lower_index + 1, n - 1)
266
+
267
+ # Interpolate if needed
268
+ fraction = index - lower_index
269
+ lower_value = sorted_values[lower_index]
270
+ upper_value = sorted_values[upper_index]
271
+
272
+ return lower_value + fraction * (upper_value - lower_value)
273
+
274
+
209
275
  # TEST DB CONNECTION, PERMISSIONS...
210
276
  # from hockey_blast_common_lib.db_connection import create_session
211
277
  # session = create_session("frontend")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hockey-blast-common-lib
3
- Version: 0.1.64
3
+ Version: 0.1.66
4
4
  Summary: Common library for shared functionality and DB models
5
5
  Author: Pavel Kletskov
6
6
  Author-email: kletskov@gmail.com
@@ -1,29 +1,31 @@
1
1
  hockey_blast_common_lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- hockey_blast_common_lib/aggregate_all_stats.py,sha256=ozILLB3-CRABYN9JHeH2sFeXw-sFhkXboU7sKTV2Ok8,1378
3
- hockey_blast_common_lib/aggregate_goalie_stats.py,sha256=QPza_glSHcggt0KTwVB6USTkvEdP3yZj7GjpNeGjFE8,17433
2
+ hockey_blast_common_lib/aggregate_all_stats.py,sha256=lWDhdYMYFEdNFTM3FmAKWiHFYSkb0OLjTkagguHlwls,1914
3
+ hockey_blast_common_lib/aggregate_goalie_stats.py,sha256=Z_xRHR-C6_2KO67LmIW8uKVD4tpEXXvFhfo0DHouuHo,19871
4
4
  hockey_blast_common_lib/aggregate_h2h_stats.py,sha256=nStyIm_be25pKDYbPCaOSHFTjbaMLFxFAa2mTU1tL_k,11486
5
5
  hockey_blast_common_lib/aggregate_human_stats.py,sha256=uoGBkROBKh8n18TyzZ6vHX_viCTpHbRsiVLyflJq92g,29247
6
- hockey_blast_common_lib/aggregate_referee_stats.py,sha256=YyFFHU2FJnXyUEzkzMnJoa5O28zjtF_spJeGedaI4QA,17389
6
+ hockey_blast_common_lib/aggregate_referee_stats.py,sha256=VZVqiTfcHKtpyqUjvFBlypz2P8bIVg9wp0OK-MOu7O8,19580
7
7
  hockey_blast_common_lib/aggregate_s2s_stats.py,sha256=gB3Oi1emtBWL3bKojUhHH01gAbQTSLvgqO1WcvLI6F8,7449
8
- hockey_blast_common_lib/aggregate_scorekeeper_stats.py,sha256=r0CUsOSjeKwAEanrPSkqVufkxk9Iv_c125mKdhwR9Ns,14758
9
- hockey_blast_common_lib/aggregate_skater_stats.py,sha256=chy-LcuNIGHP85h0FiXZT3nZrbGKbcr66_j-atrucXs,30706
8
+ hockey_blast_common_lib/aggregate_scorekeeper_stats.py,sha256=NVCL5QzeIodTKs_OvDlcKDtGKnyxA_ZMlKTGfeO4H6Y,17829
9
+ hockey_blast_common_lib/aggregate_skater_stats.py,sha256=pU9ULO90165QqWWMz5leSHD9iJb5rJegBzGjZpiKYGw,34870
10
+ hockey_blast_common_lib/aggregate_team_goalie_stats.py,sha256=Yy06zDrgLDsI2QqjFzPck3mYWNhyTxSVpZHSdpDSkRE,9569
11
+ hockey_blast_common_lib/aggregate_team_skater_stats.py,sha256=cGP8eLTDD0lEkkTmUlBvSwqYfAGIfEOQREy_xtHxHKI,11962
10
12
  hockey_blast_common_lib/assign_skater_skill.py,sha256=it3jiSyUq7XpKqxzs88lyB5t1c3t1idIS_JRwq_FQoo,2810
11
13
  hockey_blast_common_lib/db_connection.py,sha256=KACyHaOMeTX9zPNztYy8uOeB1ubIUenZcEKAeD5gC24,3333
12
14
  hockey_blast_common_lib/dump_sample_db.sh,sha256=MY3lnzTXBoWd76-ZlZr9nWsKMEVgyRsUn-LZ2d1JWZs,810
13
15
  hockey_blast_common_lib/embedding_utils.py,sha256=XbJvJlq6BKE6_oLzhUKcCrx6-TM8P-xl-S1SVLr_teU,10222
14
16
  hockey_blast_common_lib/h2h_models.py,sha256=DEmQnmuacBVRNWvpRvq2RlwmhQYrT7XPOSTDNVtchr0,8597
15
- hockey_blast_common_lib/hockey_blast_sample_backup.sql.gz,sha256=u5lGgEUTbJddCd-y2hu8LsMRoZW0Ox_jNzWYqNR1sbc,4648908
17
+ hockey_blast_common_lib/hockey_blast_sample_backup.sql.gz,sha256=sh_-vvwEIiKDCtmIIzyiyaNs67gUHCTu6CGCCdBcT6Q,4648833
16
18
  hockey_blast_common_lib/models.py,sha256=RQGUq8C8eJqUB2o3QCSs14W-9B4lMTUNvwNDM-Lc6j4,21687
17
19
  hockey_blast_common_lib/options.py,sha256=wzfGWKK_dHBA_PfiOvbP_-HtdoJCR0E7DkA5_cYDb_k,1578
18
20
  hockey_blast_common_lib/progress_utils.py,sha256=7Txjpx5G4vHbnPTvNYuBA_WtrY0QFA4mDEYUDuZyY1E,3923
19
21
  hockey_blast_common_lib/restore_sample_db.sh,sha256=7W3lzRZeu9zXIu1Bvtnaw8EHc1ulHmFM4mMh86oUQJo,2205
20
22
  hockey_blast_common_lib/skills_in_divisions.py,sha256=9sGtU6SLj8BXb5R74ue1oPWa2nbk4JfJz5VmcuxetzA,8542
21
23
  hockey_blast_common_lib/skills_propagation.py,sha256=qBK84nzkn8ZQHum0bdxFQwLvdgVE7DtWoPP9cdbOmRo,20201
22
- hockey_blast_common_lib/stats_models.py,sha256=t4nBxEr__dPJlO005jKvwQRWdUJwoXsALtVmW3crtdM,31100
24
+ hockey_blast_common_lib/stats_models.py,sha256=EU7Uw9ANPNNEx4vORgJkMFdgyTXQGmGLOF7YiY8jeBY,35488
23
25
  hockey_blast_common_lib/stats_utils.py,sha256=PTZvykl1zfEcojnzDFa1J3V3F5gREmoFG1lQHLnYHgo,300
24
- hockey_blast_common_lib/utils.py,sha256=18ThTgXliIPwBPBNAY6jU9fLUpKaiOwvsr6xBGtj4vg,7159
26
+ hockey_blast_common_lib/utils.py,sha256=xHgA3Xh40i4CBVArvfW2j123XGdgrMTFqTudPQHwkho,8997
25
27
  hockey_blast_common_lib/wsgi.py,sha256=oL9lPWccKLTAYIKPJkKZV5keVE-Dgosv74CBi770NNc,786
26
- hockey_blast_common_lib-0.1.64.dist-info/METADATA,sha256=Pmli4OSe-AKxoOz3Eb0kptBpyK2dALHREZiRbTK2bhQ,318
27
- hockey_blast_common_lib-0.1.64.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
28
- hockey_blast_common_lib-0.1.64.dist-info/top_level.txt,sha256=wIR4LIkE40npoA2QlOdfCYlgFeGbsHR8Z6r0h46Vtgc,24
29
- hockey_blast_common_lib-0.1.64.dist-info/RECORD,,
28
+ hockey_blast_common_lib-0.1.66.dist-info/METADATA,sha256=ANT3HdUqzzFk-q_2ujzu9oPPmohLJFXXZdYAqj-P0Yg,318
29
+ hockey_blast_common_lib-0.1.66.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
30
+ hockey_blast_common_lib-0.1.66.dist-info/top_level.txt,sha256=wIR4LIkE40npoA2QlOdfCYlgFeGbsHR8Z6r0h46Vtgc,24
31
+ hockey_blast_common_lib-0.1.66.dist-info/RECORD,,