hockey-blast-common-lib 0.1.28__tar.gz → 0.1.31__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.
Files changed (27) hide show
  1. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.31}/PKG-INFO +1 -1
  2. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.31}/hockey_blast_common_lib/aggregate_skater_stats.py +87 -113
  3. hockey_blast_common_lib-0.1.31/hockey_blast_common_lib/assign_skater_skill.py +46 -0
  4. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.31}/hockey_blast_common_lib/hockey_blast_sample_backup.sql.gz +0 -0
  5. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.31}/hockey_blast_common_lib/models.py +5 -5
  6. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.31}/hockey_blast_common_lib/options.py +1 -0
  7. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.31}/hockey_blast_common_lib/skills_in_divisions.py +21 -59
  8. hockey_blast_common_lib-0.1.31/hockey_blast_common_lib/skills_propagation.py +368 -0
  9. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.31}/hockey_blast_common_lib/stats_models.py +51 -0
  10. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.31}/hockey_blast_common_lib.egg-info/PKG-INFO +1 -1
  11. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.31}/hockey_blast_common_lib.egg-info/SOURCES.txt +2 -0
  12. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.31}/setup.py +1 -1
  13. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.31}/MANIFEST.in +0 -0
  14. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.31}/README.md +0 -0
  15. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.31}/hockey_blast_common_lib/__init__.py +0 -0
  16. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.31}/hockey_blast_common_lib/aggregate_goalie_stats.py +0 -0
  17. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.31}/hockey_blast_common_lib/aggregate_human_stats.py +0 -0
  18. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.31}/hockey_blast_common_lib/aggregate_referee_stats.py +0 -0
  19. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.31}/hockey_blast_common_lib/db_connection.py +0 -0
  20. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.31}/hockey_blast_common_lib/dump_sample_db.sh +0 -0
  21. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.31}/hockey_blast_common_lib/restore_sample_db.sh +0 -0
  22. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.31}/hockey_blast_common_lib/utils.py +0 -0
  23. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.31}/hockey_blast_common_lib/wsgi.py +0 -0
  24. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.31}/hockey_blast_common_lib.egg-info/dependency_links.txt +0 -0
  25. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.31}/hockey_blast_common_lib.egg-info/requires.txt +0 -0
  26. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.31}/hockey_blast_common_lib.egg-info/top_level.txt +0 -0
  27. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.31}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hockey-blast-common-lib
3
- Version: 0.1.28
3
+ Version: 0.1.31
4
4
  Summary: Common library for shared functionality and DB models
5
5
  Author: Pavel Kletskov
6
6
  Author-email: kletskov@gmail.com
@@ -6,17 +6,28 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
6
6
  from datetime import datetime, timedelta
7
7
  import sqlalchemy
8
8
 
9
- from hockey_blast_common_lib.models import Game, Goal, Penalty, GameRoster
10
- from hockey_blast_common_lib.stats_models import OrgStatsSkater, DivisionStatsSkater, OrgStatsWeeklySkater, OrgStatsDailySkater, DivisionStatsWeeklySkater, DivisionStatsDailySkater
9
+ from hockey_blast_common_lib.models import Game, Goal, Penalty, GameRoster, Organization, Division, Human, Level
10
+ from hockey_blast_common_lib.stats_models import OrgStatsSkater, DivisionStatsSkater, OrgStatsWeeklySkater, OrgStatsDailySkater, DivisionStatsWeeklySkater, DivisionStatsDailySkater, LevelStatsSkater
11
11
  from hockey_blast_common_lib.db_connection import create_session
12
12
  from sqlalchemy.sql import func, case
13
- from hockey_blast_common_lib.options import not_human_names, parse_args, MIN_GAMES_FOR_ORG_STATS, MIN_GAMES_FOR_DIVISION_STATS
14
- from hockey_blast_common_lib.utils import get_org_id_from_alias, get_human_ids_by_names, get_division_ids_for_last_season_in_all_leagues
13
+ from hockey_blast_common_lib.options import not_human_names, parse_args, MIN_GAMES_FOR_ORG_STATS, MIN_GAMES_FOR_DIVISION_STATS, MIN_GAMES_FOR_LEVEL_STATS
14
+ from hockey_blast_common_lib.utils import get_org_id_from_alias, get_human_ids_by_names, get_division_ids_for_last_season_in_all_leagues, get_all_division_ids_for_org
15
15
  from sqlalchemy import func, case, and_
16
+ from collections import defaultdict
16
17
 
17
- def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_filter_out, filter_human_id=None, aggregation_window=None):
18
+ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_filter_out, debug_human_id=None, aggregation_window=None):
18
19
  human_ids_to_filter = get_human_ids_by_names(session, names_to_filter_out)
19
20
 
21
+ # Get the name of the aggregation, for debug purposes
22
+ if aggregation_type == 'org':
23
+ aggregation_name = session.query(Organization).filter(Organization.id == aggregation_id).first().organization_name
24
+ elif aggregation_type == 'division':
25
+ aggregation_name = session.query(Division).filter(Division.id == aggregation_id).first().level
26
+ elif aggregation_type == 'level':
27
+ aggregation_name = session.query(Level).filter(Level.id == aggregation_id).first().level_name
28
+ else:
29
+ aggregation_name = "Unknown"
30
+
20
31
  if aggregation_type == 'org':
21
32
  if aggregation_window == 'Daily':
22
33
  StatsModel = OrgStatsDailySkater
@@ -35,6 +46,14 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
35
46
  StatsModel = DivisionStatsSkater
36
47
  min_games = MIN_GAMES_FOR_DIVISION_STATS
37
48
  filter_condition = Game.division_id == aggregation_id
49
+ elif aggregation_type == 'level':
50
+ StatsModel = LevelStatsSkater
51
+ min_games = MIN_GAMES_FOR_LEVEL_STATS
52
+ filter_condition = Division.level_id == aggregation_id
53
+ # Add filter to only include games for the last 5 years
54
+ # five_years_ago = datetime.now() - timedelta(days=5*365)
55
+ # level_window_filter = func.cast(func.concat(Game.date, ' ', Game.time), sqlalchemy.types.TIMESTAMP) >= five_years_ago
56
+ # filter_condition = filter_condition & level_window_filter
38
57
  else:
39
58
  raise ValueError("Invalid aggregation type")
40
59
 
@@ -59,8 +78,8 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
59
78
 
60
79
  # Filter for specific human_id if provided
61
80
  human_filter = []
62
- if filter_human_id:
63
- human_filter = [GameRoster.human_id == filter_human_id]
81
+ # if debug_human_id:
82
+ # human_filter = [GameRoster.human_id == debug_human_id]
64
83
 
65
84
  # Aggregate games played for each human in each division, excluding goalies
66
85
  games_played_stats = session.query(
@@ -68,7 +87,7 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
68
87
  GameRoster.human_id,
69
88
  func.count(Game.id).label('games_played'),
70
89
  func.array_agg(Game.id).label('game_ids')
71
- ).join(GameRoster, Game.id == GameRoster.game_id).filter(filter_condition, ~GameRoster.role.ilike('g'), *human_filter).group_by(Game.org_id, GameRoster.human_id).all()
90
+ ).join(GameRoster, Game.id == GameRoster.game_id).join(Division, Game.division_id == Division.id).filter(filter_condition, ~GameRoster.role.ilike('g'), *human_filter).group_by(Game.org_id, GameRoster.human_id).all()
72
91
 
73
92
  # Aggregate goals for each human in each division, excluding goalies
74
93
  goals_stats = session.query(
@@ -76,7 +95,7 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
76
95
  Goal.goal_scorer_id.label('human_id'),
77
96
  func.count(Goal.id).label('goals'),
78
97
  func.array_agg(Goal.game_id).label('goal_game_ids')
79
- ).join(Game, Game.id == Goal.game_id).join(GameRoster, and_(Game.id == GameRoster.game_id, Goal.goal_scorer_id == GameRoster.human_id)).filter(filter_condition, ~GameRoster.role.ilike('g'), *human_filter).group_by(Game.org_id, Goal.goal_scorer_id).all()
98
+ ).join(Game, Game.id == Goal.game_id).join(GameRoster, and_(Game.id == GameRoster.game_id, Goal.goal_scorer_id == GameRoster.human_id)).join(Division, Game.division_id == Division.id).filter(filter_condition, ~GameRoster.role.ilike('g'), *human_filter).group_by(Game.org_id, Goal.goal_scorer_id).all()
80
99
 
81
100
  # Aggregate assists for each human in each division, excluding goalies
82
101
  assists_stats = session.query(
@@ -84,14 +103,14 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
84
103
  Goal.assist_1_id.label('human_id'),
85
104
  func.count(Goal.id).label('assists'),
86
105
  func.array_agg(Goal.game_id).label('assist_game_ids')
87
- ).join(Game, Game.id == Goal.game_id).join(GameRoster, and_(Game.id == GameRoster.game_id, Goal.assist_1_id == GameRoster.human_id)).filter(filter_condition, ~GameRoster.role.ilike('g'), *human_filter).group_by(Game.org_id, Goal.assist_1_id).all()
106
+ ).join(Game, Game.id == Goal.game_id).join(GameRoster, and_(Game.id == GameRoster.game_id, Goal.assist_1_id == GameRoster.human_id)).join(Division, Game.division_id == Division.id).filter(filter_condition, ~GameRoster.role.ilike('g'), *human_filter).group_by(Game.org_id, Goal.assist_1_id).all()
88
107
 
89
108
  assists_stats_2 = session.query(
90
109
  Game.org_id,
91
110
  Goal.assist_2_id.label('human_id'),
92
111
  func.count(Goal.id).label('assists'),
93
112
  func.array_agg(Goal.game_id).label('assist_2_game_ids')
94
- ).join(Game, Game.id == Goal.game_id).join(GameRoster, and_(Game.id == GameRoster.game_id, Goal.assist_2_id == GameRoster.human_id)).filter(filter_condition, ~GameRoster.role.ilike('g'), *human_filter).group_by(Game.org_id, Goal.assist_2_id).all()
113
+ ).join(Game, Game.id == Goal.game_id).join(GameRoster, and_(Game.id == GameRoster.game_id, Goal.assist_2_id == GameRoster.human_id)).join(Division, Game.division_id == Division.id).filter(filter_condition, ~GameRoster.role.ilike('g'), *human_filter).group_by(Game.org_id, Goal.assist_2_id).all()
95
114
 
96
115
  # Aggregate penalties for each human in each division, excluding goalies
97
116
  penalties_stats = session.query(
@@ -99,7 +118,7 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
99
118
  Penalty.penalized_player_id.label('human_id'),
100
119
  func.count(Penalty.id).label('penalties'),
101
120
  func.array_agg(Penalty.game_id).label('penalty_game_ids')
102
- ).join(Game, Game.id == Penalty.game_id).join(GameRoster, and_(Game.id == GameRoster.game_id, Penalty.penalized_player_id == GameRoster.human_id)).filter(filter_condition, ~GameRoster.role.ilike('g'), *human_filter).group_by(Game.org_id, Penalty.penalized_player_id).all()
121
+ ).join(Game, Game.id == Penalty.game_id).join(GameRoster, and_(Game.id == GameRoster.game_id, Penalty.penalized_player_id == GameRoster.human_id)).join(Division, Game.division_id == Division.id).filter(filter_condition, ~GameRoster.role.ilike('g'), *human_filter).group_by(Game.org_id, Penalty.penalized_player_id).all()
103
122
 
104
123
  # Combine the results
105
124
  stats_dict = {}
@@ -107,6 +126,8 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
107
126
  if stat.human_id in human_ids_to_filter:
108
127
  continue
109
128
  key = (aggregation_id, stat.human_id)
129
+ if stat.games_played < min_games:
130
+ continue
110
131
  stats_dict[key] = {
111
132
  'games_played': stat.games_played,
112
133
  'goals': 0,
@@ -123,94 +144,26 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
123
144
  }
124
145
 
125
146
  for stat in goals_stats:
126
- if stat.human_id in human_ids_to_filter:
127
- continue
128
147
  key = (aggregation_id, stat.human_id)
129
- if key not in stats_dict:
130
- stats_dict[key] = {
131
- 'games_played': 0,
132
- 'goals': stat.goals,
133
- 'assists': 0,
134
- 'penalties': 0,
135
- 'points': stat.goals, # Initialize points with goals
136
- 'goals_per_game': 0.0,
137
- 'points_per_game': 0.0,
138
- 'assists_per_game': 0.0,
139
- 'penalties_per_game': 0.0,
140
- 'game_ids': [],
141
- 'first_game_id': None,
142
- 'last_game_id': None
143
- }
144
- else:
148
+ if key in stats_dict:
145
149
  stats_dict[key]['goals'] += stat.goals
146
150
  stats_dict[key]['points'] += stat.goals # Update points
147
151
 
148
152
  for stat in assists_stats:
149
- if stat.human_id in human_ids_to_filter:
150
- continue
151
153
  key = (aggregation_id, stat.human_id)
152
- if key not in stats_dict:
153
- stats_dict[key] = {
154
- 'games_played': 0,
155
- 'goals': 0,
156
- 'assists': stat.assists,
157
- 'penalties': 0,
158
- 'points': stat.assists, # Initialize points with assists
159
- 'goals_per_game': 0.0,
160
- 'points_per_game': 0.0,
161
- 'assists_per_game': 0.0,
162
- 'penalties_per_game': 0.0,
163
- 'game_ids': [],
164
- 'first_game_id': None,
165
- 'last_game_id': None
166
- }
167
- else:
154
+ if key in stats_dict:
168
155
  stats_dict[key]['assists'] += stat.assists
169
156
  stats_dict[key]['points'] += stat.assists # Update points
170
157
 
171
158
  for stat in assists_stats_2:
172
- if stat.human_id in human_ids_to_filter:
173
- continue
174
159
  key = (aggregation_id, stat.human_id)
175
- if key not in stats_dict:
176
- stats_dict[key] = {
177
- 'games_played': 0,
178
- 'goals': 0,
179
- 'assists': stat.assists,
180
- 'penalties': 0,
181
- 'points': stat.assists, # Initialize points with assists
182
- 'goals_per_game': 0.0,
183
- 'points_per_game': 0.0,
184
- 'assists_per_game': 0.0,
185
- 'penalties_per_game': 0.0,
186
- 'game_ids': [],
187
- 'first_game_id': None,
188
- 'last_game_id': None
189
- }
190
- else:
160
+ if key in stats_dict:
191
161
  stats_dict[key]['assists'] += stat.assists
192
162
  stats_dict[key]['points'] += stat.assists # Update points
193
163
 
194
164
  for stat in penalties_stats:
195
- if stat.human_id in human_ids_to_filter:
196
- continue
197
165
  key = (aggregation_id, stat.human_id)
198
- if key not in stats_dict:
199
- stats_dict[key] = {
200
- 'games_played': 0,
201
- 'goals': 0,
202
- 'assists': 0,
203
- 'penalties': stat.penalties,
204
- 'points': 0, # Initialize points
205
- 'goals_per_game': 0.0,
206
- 'points_per_game': 0.0,
207
- 'assists_per_game': 0.0,
208
- 'penalties_per_game': 0.0,
209
- 'game_ids': [],
210
- 'first_game_id': None,
211
- 'last_game_id': None
212
- }
213
- else:
166
+ if key in stats_dict:
214
167
  stats_dict[key]['penalties'] += stat.penalties
215
168
 
216
169
  # Calculate per game stats
@@ -233,20 +186,10 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
233
186
  stat['first_game_id'] = first_game.id if first_game else None
234
187
  stat['last_game_id'] = last_game.id if last_game else None
235
188
 
236
- # Debug output for totals if filter_human_id is provided
237
- if filter_human_id:
238
- for key, stat in stats_dict.items():
239
- if key[1] == filter_human_id:
240
- print(f"Human ID: {filter_human_id}")
241
- print(f"Total Games Played: {stat['games_played']}")
242
- print(f"Total Goals: {stat['goals']}")
243
- print(f"Total Assists: {stat['assists']}")
244
- print(f"Total Penalties: {stat['penalties']}")
245
-
246
189
  # Calculate total_in_rank
247
190
  total_in_rank = len(stats_dict)
248
191
 
249
- # Assign ranks
192
+ # Assign ranks within each level
250
193
  def assign_ranks(stats_dict, field):
251
194
  sorted_stats = sorted(stats_dict.items(), key=lambda x: x[1][field], reverse=True)
252
195
  for rank, (key, stat) in enumerate(sorted_stats, start=1):
@@ -262,13 +205,22 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
262
205
  assign_ranks(stats_dict, 'assists_per_game')
263
206
  assign_ranks(stats_dict, 'penalties_per_game')
264
207
 
208
+ # Debug output for specific human
209
+ if debug_human_id:
210
+ if any(key[1] == debug_human_id for key in stats_dict):
211
+ human = session.query(Human).filter(Human.id == debug_human_id).first()
212
+ human_name = f"{human.first_name} {human.last_name}" if human else "Unknown"
213
+ print(f"For Human {debug_human_id} ({human_name}) for {aggregation_type} {aggregation_id} ({aggregation_name}) , total_in_rank {total_in_rank} and window {aggregation_window}:")
214
+ for key, stat in stats_dict.items():
215
+ if key[1] == debug_human_id:
216
+ for k, v in stat.items():
217
+ print(f"{k}: {v}")
218
+
265
219
  # Insert aggregated stats into the appropriate table with progress output
266
220
  total_items = len(stats_dict)
267
221
  batch_size = 1000
268
222
  for i, (key, stat) in enumerate(stats_dict.items(), 1):
269
223
  aggregation_id, human_id = key
270
- if stat['games_played'] < min_games:
271
- continue
272
224
  goals_per_game = stat['goals'] / stat['games_played'] if stat['games_played'] > 0 else 0.0
273
225
  points_per_game = (stat['goals'] + stat['assists']) / stat['games_played'] if stat['games_played'] > 0 else 0.0
274
226
  assists_per_game = stat['assists'] / stat['games_played'] if stat['games_played'] > 0 else 0.0
@@ -302,25 +254,47 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
302
254
  # Commit in batches
303
255
  if i % batch_size == 0:
304
256
  session.commit()
305
- print(f"\r{i}/{total_items} ({(i/total_items)*100:.2f}%)", end="")
257
+ if debug_human_id is None:
258
+ print(f"\r{i}/{total_items} ({(i/total_items)*100:.2f}%)", end="")
259
+
306
260
  session.commit()
307
- print(f"\r{total_items}/{total_items} (100.00%)")
308
- print("\nDone.")
261
+ if debug_human_id is None:
262
+ print(f"\r{total_items}/{total_items} (100.00%)")
309
263
 
310
- # Example usage
311
264
  if __name__ == "__main__":
312
- args = parse_args()
313
- org_alias = args.org
314
265
  session = create_session("boss")
315
- org_id = get_org_id_from_alias(session, org_alias)
266
+ human_id_to_debug = 117076
316
267
 
317
- division_ids = get_division_ids_for_last_season_in_all_leagues(session, org_id)
318
- print(f"Aggregating skater stats for {len(division_ids)} divisions in {org_alias}...")
319
- for division_id in division_ids:
320
- aggregate_skater_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, filter_human_id=None)
321
- aggregate_skater_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, filter_human_id=None, aggregation_window='Weekly')
322
- aggregate_skater_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, filter_human_id=None, aggregation_window='Daily')
268
+ # Get all org_id present in the Organization table
269
+ org_ids = session.query(Organization.id).all()
270
+ org_ids = [org_id[0] for org_id in org_ids]
323
271
 
324
- aggregate_skater_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names, filter_human_id=None)
325
- aggregate_skater_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names, filter_human_id=None, aggregation_window='Weekly')
326
- aggregate_skater_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names, filter_human_id=None, aggregation_window='Daily')
272
+ for org_id in org_ids:
273
+ division_ids = get_all_division_ids_for_org(session, org_id)
274
+ print(f"Aggregating skater stats for {len(division_ids)} divisions in org_id {org_id}...")
275
+ total_divisions = len(division_ids)
276
+ processed_divisions = 0
277
+ for division_id in division_ids:
278
+ aggregate_skater_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, debug_human_id=human_id_to_debug)
279
+ aggregate_skater_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, debug_human_id=human_id_to_debug, aggregation_window='Weekly')
280
+ aggregate_skater_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, debug_human_id=human_id_to_debug, aggregation_window='Daily')
281
+ processed_divisions += 1
282
+ if human_id_to_debug is None:
283
+ print(f"\rProcessed {processed_divisions}/{total_divisions} divisions ({(processed_divisions/total_divisions)*100:.2f}%)", end="")
284
+
285
+ aggregate_skater_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names, debug_human_id=human_id_to_debug)
286
+ aggregate_skater_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names, debug_human_id=human_id_to_debug, aggregation_window='Weekly')
287
+ aggregate_skater_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names, debug_human_id=human_id_to_debug, aggregation_window='Daily')
288
+
289
+ # Aggregate by level
290
+ level_ids = session.query(Division.level_id).distinct().all()
291
+ level_ids = [level_id[0] for level_id in level_ids]
292
+ total_levels = len(level_ids)
293
+ processed_levels = 0
294
+ for level_id in level_ids:
295
+ if level_id is None:
296
+ continue
297
+ if human_id_to_debug is None:
298
+ print(f"\rProcessed {processed_levels}/{total_levels} levels ({(processed_levels/total_levels)*100:.2f}%)", end="")
299
+ processed_levels += 1
300
+ aggregate_skater_stats(session, aggregation_type='level', aggregation_id=level_id, names_to_filter_out=not_human_names, debug_human_id=human_id_to_debug)
@@ -0,0 +1,46 @@
1
+ import sys, os
2
+
3
+ # Add the package directory to the Python path
4
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
5
+
6
+ from hockey_blast_common_lib.models import Human, LevelStatsSkater
7
+ from hockey_blast_common_lib.db_connection import create_session
8
+ from sqlalchemy.sql import func
9
+
10
+ def calculate_skater_skill_value(human_id, level_stats):
11
+ max_skill_value = 0
12
+
13
+ for stat in level_stats:
14
+ level_skill_value = stat.level.skill_value
15
+ if level_skill_value < 0:
16
+ continue
17
+ ppg_ratio = stat.points_per_game_rank / stat.total_in_rank
18
+ games_played_ratio = stat.games_played_rank / stat.total_in_rank
19
+
20
+ # Take the maximum of the two ratios
21
+ skill_value = level_skill_value * max(ppg_ratio, games_played_ratio)
22
+ max_skill_value = max(max_skill_value, skill_value)
23
+
24
+ return max_skill_value
25
+
26
+ def assign_skater_skill_values():
27
+ session = create_session("boss")
28
+
29
+ humans = session.query(Human).all()
30
+ total_humans = len(humans)
31
+ processed_humans = 0
32
+
33
+ for human in humans:
34
+ level_stats = session.query(LevelStatsSkater).filter(LevelStatsSkater.human_id == human.id).all()
35
+ if level_stats:
36
+ skater_skill_value = calculate_skater_skill_value(human.id, level_stats)
37
+ human.skater_skill_value = skater_skill_value
38
+ session.commit()
39
+
40
+ processed_humans += 1
41
+ print(f"\rProcessed {processed_humans}/{total_humans} humans ({(processed_humans/total_humans)*100:.2f}%)", end="")
42
+
43
+ print("\nSkater skill values have been assigned to all humans.")
44
+
45
+ if __name__ == "__main__":
46
+ assign_skater_skill_values()
@@ -18,9 +18,8 @@ class Division(db.Model):
18
18
  league_number = db.Column(db.Integer) # TODO: Deprecate usage and remove (get this info through Season->League)
19
19
  season_number = db.Column(db.Integer) # TODO: Deprecate usage and remove (get this info from Season by season_id)
20
20
  season_id = db.Column(db.Integer, db.ForeignKey('seasons.id'))
21
- level = db.Column(db.String(100))
22
- skill_value = db.Column(db.Float)
23
- skill_propagation_sequence = db.Column(db.Integer, nullable=True, default=-1)
21
+ level = db.Column(db.String(100)) # Obsolete, use skill_id instead
22
+ level_id = db.Column(db.Integer, db.ForeignKey('levels.id')) # New field
24
23
  org_id = db.Column(db.Integer, db.ForeignKey('organizations.id'), nullable=False)
25
24
  __table_args__ = (
26
25
  db.UniqueConstraint('org_id', 'league_number', 'season_number', 'level', name='_org_league_season_level_uc'),
@@ -154,14 +153,15 @@ class League(db.Model):
154
153
  db.UniqueConstraint('org_id', 'league_number', name='_org_league_number_uc'),
155
154
  )
156
155
 
157
- class Skill(db.Model):
158
- __tablename__ = 'skills'
156
+ class Level(db.Model):
157
+ __tablename__ = 'levels'
159
158
  id = db.Column(db.Integer, primary_key=True)
160
159
  org_id = db.Column(db.Integer, db.ForeignKey('organizations.id'), nullable=False)
161
160
  skill_value = db.Column(db.Float) # A number from 0 (NHL) to 100 (pedestrian)
162
161
  level_name = db.Column(db.String(100), unique=True)
163
162
  level_alternative_name = db.Column(db.String(100))
164
163
  is_seed = db.Column(db.Boolean, nullable=True, default=False) # New field
164
+ skill_propagation_sequence = db.Column(db.Integer, nullable=True, default=-1)
165
165
  __table_args__ = (
166
166
  db.UniqueConstraint('org_id', 'level_name', name='_org_level_name_uc'),
167
167
  )
@@ -4,6 +4,7 @@ MAX_HUMAN_SEARCH_RESULTS = 25
4
4
  MAX_TEAM_SEARCH_RESULTS = 25
5
5
  MIN_GAMES_FOR_ORG_STATS = 1
6
6
  MIN_GAMES_FOR_DIVISION_STATS = 1
7
+ MIN_GAMES_FOR_LEVEL_STATS = 20
7
8
 
8
9
  orgs = {'caha', 'sharksice', 'tvice'}
9
10
 
@@ -5,7 +5,7 @@ from collections import defaultdict
5
5
  # Add the project root directory to the Python path
6
6
  sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
7
7
 
8
- from hockey_blast_common_lib.models import Game, Division, Skill, Season, League
8
+ from hockey_blast_common_lib.models import Game, Division, Level, Season, League
9
9
  from hockey_blast_common_lib.db_connection import create_session
10
10
 
11
11
  def analyze_levels(org):
@@ -47,65 +47,27 @@ def analyze_levels(org):
47
47
 
48
48
  session.close()
49
49
 
50
- def reset_skill_values_in_divisions():
51
- session = create_session("boss")
52
-
53
- # Fetch all records from the Division table
54
- divisions = session.query(Division).all()
55
-
56
- for division in divisions:
57
- # Look up the Skill table using the level from Division
58
- div_level = division.level
59
-
60
- # Query to find the matching Skill
61
- skill = session.query(Skill).filter(Skill.org_id == division.org_id, Skill.level_name == div_level).one_or_none()
62
-
63
- if not skill:
64
- # If no match found, check each alternative name individually
65
- skills = session.query(Skill).filter(Skill.org_id == division.org_id).all()
66
- for s in skills:
67
- alternative_names = s.level_alternative_name.split(',')
68
- if div_level in alternative_names:
69
- skill = s
70
- break
71
-
72
- if skill:
73
- # Assign the skill_value and set skill_propagation_sequence to 0
74
- division.skill_value = skill.skill_value
75
- division.skill_propagation_sequence = 0
76
- print(f"Assigned Skill {skill.skill_value} for Division with name {division.level}")
77
- else:
78
- # Assign -1 to skill_value and skill_propagation_sequence
79
- division.skill_value = -1
80
- division.skill_propagation_sequence = -1
81
- print(f"No matching Skill found for Division with name {division.level}, assigned default values")
82
-
83
- # Commit the changes to the Division
84
- session.commit()
85
-
86
- print("Skill values and propagation sequences have been populated into the Division table.")
87
-
88
50
  def fill_seed_skills():
89
51
  session = create_session("boss")
90
52
 
91
53
  # List of Skill objects based on the provided comments
92
54
  skills = [
93
- Skill(is_seed=True, org_id=1, skill_value=10.0, level_name='Adult Division 1', level_alternative_name='Senior A'),
94
- Skill(is_seed=True, org_id=1, skill_value=20.0, level_name='Adult Division 2', level_alternative_name='Senior B'),
95
- Skill(is_seed=True, org_id=1, skill_value=30.0, level_name='Adult Division 3A', level_alternative_name='Senior BB'),
96
- Skill(is_seed=True, org_id=1, skill_value=35.0, level_name='Adult Division 3B', level_alternative_name='Senior C'),
97
- Skill(is_seed=True, org_id=1, skill_value=40.0, level_name='Adult Division 4A', level_alternative_name='Senior CC'),
98
- Skill(is_seed=True, org_id=1, skill_value=45.0, level_name='Adult Division 4B', level_alternative_name='Senior CCC,Senior CCCC'),
99
- Skill(is_seed=True, org_id=1, skill_value=50.0, level_name='Adult Division 5A', level_alternative_name='Senior D,Senior DD'),
100
- Skill(is_seed=True, org_id=1, skill_value=55.0, level_name='Adult Division 5B', level_alternative_name='Senior DDD'),
101
- Skill(is_seed=True, org_id=1, skill_value=60.0, level_name='Adult Division 6A', level_alternative_name='Senior DDDD'),
102
- Skill(is_seed=True, org_id=1, skill_value=65.0, level_name='Adult Division 6B', level_alternative_name='Senior DDDDD'),
103
- Skill(is_seed=True, org_id=1, skill_value=70.0, level_name='Adult Division 7A', level_alternative_name='Senior E'),
104
- Skill(is_seed=True, org_id=1, skill_value=75.0, level_name='Adult Division 7B', level_alternative_name='Senior EE'),
105
- Skill(is_seed=True, org_id=1, skill_value=80.0, level_name='Adult Division 8', level_alternative_name='Senior EEE'),
106
- Skill(is_seed=True, org_id=1, skill_value=80.0, level_name='Adult Division 8A', level_alternative_name='Senior EEE'),
107
- Skill(is_seed=True, org_id=1, skill_value=85.0, level_name='Adult Division 8B', level_alternative_name='Senior EEEE'),
108
- Skill(is_seed=True, org_id=1, skill_value=90.0, level_name='Adult Division 9', level_alternative_name='Senior EEEEE')
55
+ Level(is_seed=True, org_id=1, skill_value=10.0, level_name='Adult Division 1', level_alternative_name='Senior A'),
56
+ Level(is_seed=True, org_id=1, skill_value=20.0, level_name='Adult Division 2', level_alternative_name='Senior B'),
57
+ Level(is_seed=True, org_id=1, skill_value=30.0, level_name='Adult Division 3A', level_alternative_name='Senior BB'),
58
+ Level(is_seed=True, org_id=1, skill_value=35.0, level_name='Adult Division 3B', level_alternative_name='Senior C'),
59
+ Level(is_seed=True, org_id=1, skill_value=40.0, level_name='Adult Division 4A', level_alternative_name='Senior CC'),
60
+ Level(is_seed=True, org_id=1, skill_value=45.0, level_name='Adult Division 4B', level_alternative_name='Senior CCC,Senior CCCC'),
61
+ Level(is_seed=True, org_id=1, skill_value=50.0, level_name='Adult Division 5A', level_alternative_name='Senior D,Senior DD'),
62
+ Level(is_seed=True, org_id=1, skill_value=55.0, level_name='Adult Division 5B', level_alternative_name='Senior DDD'),
63
+ Level(is_seed=True, org_id=1, skill_value=60.0, level_name='Adult Division 6A', level_alternative_name='Senior DDDD'),
64
+ Level(is_seed=True, org_id=1, skill_value=65.0, level_name='Adult Division 6B', level_alternative_name='Senior DDDDD'),
65
+ Level(is_seed=True, org_id=1, skill_value=70.0, level_name='Adult Division 7A', level_alternative_name='Senior E'),
66
+ Level(is_seed=True, org_id=1, skill_value=75.0, level_name='Adult Division 7B', level_alternative_name='Senior EE'),
67
+ Level(is_seed=True, org_id=1, skill_value=80.0, level_name='Adult Division 8', level_alternative_name='Senior EEE'),
68
+ Level(is_seed=True, org_id=1, skill_value=80.0, level_name='Adult Division 8A', level_alternative_name='Senior EEE'),
69
+ Level(is_seed=True, org_id=1, skill_value=85.0, level_name='Adult Division 8B', level_alternative_name='Senior EEEE'),
70
+ Level(is_seed=True, org_id=1, skill_value=90.0, level_name='Adult Division 9', level_alternative_name='Senior EEEEE')
109
71
  ]
110
72
 
111
73
  for skill in skills:
@@ -116,9 +78,9 @@ def fill_seed_skills():
116
78
 
117
79
  def get_fake_skill(session):
118
80
  # Create a special fake Skill with org_id == -1 and skill_value == -1
119
- fake_skill = session.query(Skill).filter_by(org_id=1, level_name='Fake Skill').first()
81
+ fake_skill = session.query(Level).filter_by(org_id=1, level_name='Fake Skill').first()
120
82
  if not fake_skill:
121
- fake_skill = Skill(
83
+ fake_skill = Level(
122
84
  org_id=1,
123
85
  skill_value=-1,
124
86
  level_name='Fake Skill',
@@ -143,7 +105,7 @@ def delete_all_skills():
143
105
  fake_skill = get_fake_skill(session)
144
106
  assign_fake_skill_to_divisions(session, fake_skill)
145
107
  # Delete all Skill records except the fake skill
146
- session.query(Skill).filter(Skill.id != fake_skill.id).delete(synchronize_session=False)
108
+ session.query(Level).filter(Level.id != fake_skill.id).delete(synchronize_session=False)
147
109
  session.commit()
148
110
  print("All Skill records except the fake skill have been deleted.")
149
111
 
@@ -177,7 +139,7 @@ def populate_league_ids():
177
139
 
178
140
  if __name__ == "__main__":
179
141
  # delete_all_skills()
180
- # fill_seed_skills()
142
+ #fill_seed_skills()
181
143
  reset_skill_values_in_divisions()
182
144
  #populate_season_ids() # Call the function to populate season_ids
183
145
  #populate_league_ids() # Call the new function to populate league_ids
@@ -0,0 +1,368 @@
1
+ import sys
2
+ import os
3
+ from collections import defaultdict
4
+ import numpy as np
5
+
6
+ # Add the project root directory to the Python path
7
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
8
+
9
+ from hockey_blast_common_lib.models import Level, Division
10
+ from hockey_blast_common_lib.stats_models import LevelsGraphEdge, LevelStatsSkater, SkillValuePPGRatio
11
+ from hockey_blast_common_lib.db_connection import create_session
12
+ from sqlalchemy import func
13
+
14
+ import numpy as np
15
+
16
+ class Config:
17
+ MIN_GAMES_PLAYED_FOR_EDGE = 10
18
+ MIN_PPG_FOR_EDGE = 0.5
19
+ MIN_HUMANS_FOR_EDGE = 2
20
+ MAX_PROPAGATION_SEQUENCE = 4
21
+ MIN_CONNECTIONS_FOR_CORRELATION = 20
22
+ MIN_CONNECTIONS_FOR_PROPAGATION = 5
23
+ MAX_SKILL_DIFF_IN_EDGE = 30
24
+
25
+ @staticmethod
26
+ def discard_outliers(data, m=2):
27
+ """
28
+ Discard outliers from the data using the modified Z-score method.
29
+ :param data: List of data points
30
+ :param m: Threshold for the modified Z-score
31
+ :return: List of data points with outliers removed
32
+ """
33
+ if len(data) == 0:
34
+ return data
35
+ median = np.median(data)
36
+ diff = np.abs(data - median)
37
+ med_abs_deviation = np.median(diff)
38
+ if med_abs_deviation == 0:
39
+ return data
40
+ modified_z_score = 0.6745 * diff / med_abs_deviation
41
+ return data[modified_z_score < m]
42
+
43
+ def reset_skill_values_in_divisions():
44
+ session = create_session("boss")
45
+
46
+ # Fetch all records from the Division table
47
+ divisions = session.query(Division).all()
48
+
49
+ for division in divisions:
50
+ # Look up the Skill table using the level from Division
51
+ div_level = division.level
52
+ # Query to find the matching Skill
53
+ level = session.query(Level).filter(Level.org_id == division.org_id, Level.level_name == div_level).one_or_none()
54
+
55
+ if not level:
56
+ # If no match found, check each alternative name individually
57
+ skills = session.query(Level).filter(Level.org_id == division.org_id).all()
58
+ for s in skills:
59
+ alternative_names = s.level_alternative_name.split(',')
60
+ if div_level in alternative_names:
61
+ level = s
62
+ break
63
+
64
+ if level:
65
+ # Assign the skill_value and set skill_propagation_sequence to 0
66
+ division.level_id = level.id
67
+ if level.is_seed:
68
+ level.skill_propagation_sequence = 0
69
+ else:
70
+ level.skill_propagation_sequence = -1
71
+ level.skill_value = -1
72
+ else:
73
+ # Add new Skill with values previously used for division
74
+ new_level = Level(
75
+ org_id=division.org_id,
76
+ skill_value=-1,
77
+ level_name=division.level,
78
+ level_alternative_name='',
79
+ is_seed=False,
80
+ skill_propagation_sequence=-1
81
+ )
82
+ session.add(new_level)
83
+ session.commit()
84
+ division.skill_id = new_level.id
85
+ print(f"Created new Level for Division {division.level}")
86
+
87
+ # Commit the changes to the Division
88
+ session.commit()
89
+
90
+ print("Level values and propagation sequences have been populated into the Division table.")
91
+
92
+ def build_levels_graph_edges():
93
+ # Creates unique edges from levelA to levelB (there is no reverse edge levelB to levelA)
94
+ session = create_session("boss")
95
+
96
+ # Delete all existing edges
97
+ session.query(LevelsGraphEdge).delete()
98
+ session.commit()
99
+
100
+ # Query to get all level stats
101
+ level_stats = session.query(LevelStatsSkater).all()
102
+
103
+ # Dictionary to store stats by level and human
104
+ level_human_stats = defaultdict(lambda: defaultdict(dict))
105
+
106
+ for stat in level_stats:
107
+ if stat.games_played >= Config.MIN_GAMES_PLAYED_FOR_EDGE and stat.points_per_game >= Config.MIN_PPG_FOR_EDGE:
108
+ level_human_stats[stat.level_id][stat.human_id] = {
109
+ 'games_played': stat.games_played,
110
+ 'points_per_game': stat.points_per_game
111
+ }
112
+
113
+ # Dictionary to store edges
114
+ edges = {}
115
+
116
+ # Build edges
117
+ total_levels = len(level_human_stats)
118
+ processed_levels = 0
119
+ for from_level_id, from_humans in level_human_stats.items():
120
+ from_level = session.query(Level).filter_by(id=from_level_id).first()
121
+ for to_level_id, to_humans in level_human_stats.items():
122
+ to_level = session.query(Level).filter_by(id=to_level_id).first()
123
+ if from_level.id >= to_level.id:
124
+ continue
125
+
126
+ common_humans = set(from_humans.keys()) & set(to_humans.keys())
127
+ n_connections = len(common_humans)
128
+ n_games = 0
129
+
130
+ if n_connections < Config.MIN_HUMANS_FOR_EDGE:
131
+ continue
132
+
133
+ ppg_ratios = []
134
+ # if from_level.id == 223 and to_level.id == 219: #216
135
+ # print(f"Debug: From Level ID: {from_level.id}, To Level ID: {to_level.id}")
136
+ for human_id in common_humans:
137
+ from_ppg = from_humans[human_id]['points_per_game']
138
+ to_ppg = to_humans[human_id]['points_per_game']
139
+ from_games = from_humans[human_id]['games_played']
140
+ to_games = to_humans[human_id]['games_played']
141
+ min_games = min(from_games, to_games)
142
+ n_games += min_games
143
+
144
+ # if from_level.id == 223 and to_level.id == 219: #216
145
+ # print(f"Human {human_id} From PPG: {from_ppg}, To PPG: {to_ppg}, Min Games: {min_games} n_games: {n_games}")
146
+
147
+ if from_ppg > 0 and to_ppg > 0:
148
+ ppg_ratios.append(to_ppg / from_ppg)
149
+
150
+ if not ppg_ratios:
151
+ continue
152
+
153
+ # Discard outliers
154
+ ppg_ratios = Config.discard_outliers(np.array(ppg_ratios))
155
+
156
+ if len(ppg_ratios) == 0:
157
+ continue
158
+
159
+ avg_ppg_ratio = float(sum(ppg_ratios) / len(ppg_ratios))
160
+
161
+ # if sorted([from_level.id, to_level.id]) == [219, 223]:
162
+ # print(f"From {from_level_id} to {to_level_id} n_connections {n_connections} n_games: {n_games}")
163
+
164
+ edge = LevelsGraphEdge(
165
+ from_level_id=from_level_id,
166
+ to_level_id=to_level_id,
167
+ n_connections=n_connections,
168
+ ppg_ratio=avg_ppg_ratio,
169
+ n_games=n_games # Store the number of games
170
+ )
171
+ edges[(from_level_id, to_level_id)] = edge
172
+
173
+ processed_levels += 1
174
+ print(f"\rProcessed {processed_levels}/{total_levels} levels ({(processed_levels/total_levels)*100:.2f}%)", end="")
175
+
176
+ # Insert edges into the database
177
+ for edge in edges.values():
178
+ session.add(edge)
179
+ session.commit()
180
+
181
+ print("\nLevels graph edges have been populated into the database.")
182
+
183
+ def propagate_skill_levels(propagation_sequence):
184
+ min_skill_value = float('inf')
185
+ max_skill_value = float('-inf')
186
+
187
+ session = create_session("boss")
188
+
189
+ if propagation_sequence == 0:
190
+ # Delete all existing correlation data
191
+ session.query(SkillValuePPGRatio).delete()
192
+ session.commit()
193
+
194
+ # Build and save the correlation data
195
+ levels = session.query(Level).filter(Level.skill_propagation_sequence == 0).all()
196
+ level_ids = {level.id for level in levels}
197
+ correlation_data = defaultdict(list)
198
+
199
+ for level in levels:
200
+ if level.skill_value == -1:
201
+ continue
202
+
203
+ edges = session.query(LevelsGraphEdge).filter(
204
+ (LevelsGraphEdge.from_level_id == level.id) |
205
+ (LevelsGraphEdge.to_level_id == level.id)
206
+ ).all()
207
+
208
+ for edge in edges:
209
+ if edge.n_connections < Config.MIN_CONNECTIONS_FOR_CORRELATION:
210
+ continue
211
+
212
+ if edge.from_level_id == level.id:
213
+ target_level_id = edge.to_level_id
214
+ ppg_ratio_edge = edge.ppg_ratio
215
+ else:
216
+ # We go over same edge twice in this logic, let's skip the reverse edge
217
+ continue
218
+
219
+ if target_level_id not in level_ids:
220
+ continue
221
+
222
+ target_level = session.query(Level).filter_by(id=target_level_id).first()
223
+ if target_level:
224
+ skill_value_from = level.skill_value
225
+ skill_value_to = target_level.skill_value
226
+
227
+ # Same skill value - no correlation
228
+ if skill_value_from == skill_value_to:
229
+ continue
230
+
231
+
232
+ # Since we go over all levels in the sequence 0, we will see each edge twice
233
+ # This condition eliminates duplicates
234
+ if abs(skill_value_from - skill_value_to) > Config.MAX_SKILL_DIFF_IN_EDGE:
235
+ continue
236
+
237
+ # Debug prints
238
+ # print(f"From Skill {level.skill_value} to {target_level.skill_value} ratio: {ppg_ratio}")
239
+
240
+ # Ensure INCREASING SKILL VALUES for the correlation data!
241
+ if skill_value_from > skill_value_to:
242
+ skill_value_from, skill_value_to = skill_value_to, skill_value_from
243
+ ppg_ratio_edge = 1 / ppg_ratio_edge
244
+
245
+ correlation_data[(skill_value_from, skill_value_to)].append(
246
+ (ppg_ratio_edge, edge.n_games)
247
+ )
248
+
249
+ # Save correlation data to the database
250
+ for (skill_value_from, skill_value_to), ppg_ratios in correlation_data.items():
251
+ ppg_ratios = [(ppg_ratio, n_games) for ppg_ratio, n_games in ppg_ratios]
252
+ ppg_ratios_array = np.array(ppg_ratios, dtype=[('ppg_ratio', float), ('n_games', int)])
253
+ ppg_ratios_filtered = Config.discard_outliers(ppg_ratios_array['ppg_ratio'])
254
+ if len(ppg_ratios_filtered) > 0:
255
+ avg_ppg_ratio = float(sum(ppg_ratio * n_games for ppg_ratio, n_games in ppg_ratios if ppg_ratio in ppg_ratios_filtered) / sum(n_games for ppg_ratio, n_games in ppg_ratios if ppg_ratio in ppg_ratios_filtered))
256
+ total_n_games = sum(n_games for ppg_ratio, n_games in ppg_ratios if ppg_ratio in ppg_ratios_filtered)
257
+ correlation = SkillValuePPGRatio(
258
+ from_skill_value=skill_value_from,
259
+ to_skill_value=skill_value_to,
260
+ ppg_ratio=avg_ppg_ratio,
261
+ n_games=total_n_games # Store the sum of games
262
+ )
263
+ session.add(correlation)
264
+ session.commit()
265
+ # Update min and max skill values
266
+ min_skill_value = min(min_skill_value, skill_value_from, skill_value_to)
267
+ max_skill_value = max(max_skill_value, skill_value_from, skill_value_to)
268
+
269
+ # Propagate skill levels
270
+ levels = session.query(Level).filter(Level.skill_propagation_sequence == propagation_sequence).all()
271
+ suggested_skill_values = defaultdict(list)
272
+
273
+ for level in levels:
274
+ edges = session.query(LevelsGraphEdge).filter(
275
+ (LevelsGraphEdge.from_level_id == level.id) |
276
+ (LevelsGraphEdge.to_level_id == level.id)
277
+ ).all()
278
+
279
+ for edge in edges:
280
+ if edge.n_connections < Config.MIN_CONNECTIONS_FOR_PROPAGATION:
281
+ continue
282
+
283
+ if edge.from_level_id == level.id:
284
+ target_level_id = edge.to_level_id
285
+ ppg_ratio_edge = edge.ppg_ratio
286
+ else:
287
+ target_level_id = edge.from_level_id
288
+ ppg_ratio_edge = 1 / edge.ppg_ratio
289
+
290
+ target_level = session.query(Level).filter_by(id=target_level_id).first()
291
+ if target_level and target_level.skill_propagation_sequence == -1:
292
+ correlations = session.query(SkillValuePPGRatio).filter(
293
+ (SkillValuePPGRatio.from_skill_value <= level.skill_value) &
294
+ (SkillValuePPGRatio.to_skill_value >= level.skill_value)
295
+ ).all()
296
+
297
+ if correlations:
298
+ weighted_skill_values = []
299
+ for correlation in correlations:
300
+ # Skill value always increases in the correlation data
301
+ # Let's avoid extrapolating from the end of the edge and away from the edge!
302
+
303
+ # Check left side of the edge
304
+ if (level.skill_value == correlation.from_skill_value and level.skill_value > min_skill_value):
305
+ if ppg_ratio_edge < 1:
306
+ continue
307
+ # Check right side of the edge
308
+ if (level.skill_value == correlation.to_skill_value and level.skill_value < max_skill_value):
309
+ if ppg_ratio_edge > 1:
310
+ continue
311
+
312
+
313
+ # First confirm which way are we going here
314
+ if (ppg_ratio_edge < 1 and correlation.ppg_ratio > 1) or (ppg_ratio_edge > 1 and correlation.ppg_ratio < 1):
315
+ # Reverse the correlation
316
+ from_skill_value=correlation.to_skill_value
317
+ to_skill_value=correlation.from_skill_value
318
+ ppg_ratio_range = 1 / correlation.ppg_ratio
319
+ else:
320
+ from_skill_value=correlation.from_skill_value
321
+ to_skill_value=correlation.to_skill_value
322
+ ppg_ratio_range = correlation.ppg_ratio
323
+
324
+ # Now both ratios are either < 1 or > 1
325
+ if ppg_ratio_edge < 1:
326
+ ppg_ratio_for_extrapolation = 1 / ppg_ratio_edge
327
+ ppg_ratio_range = 1 / ppg_ratio_range
328
+ else:
329
+ ppg_ratio_for_extrapolation = ppg_ratio_edge
330
+
331
+ # Interpolate or extrapolate skill value
332
+ skill_value_range = to_skill_value - from_skill_value
333
+ skill_value_diff = (ppg_ratio_for_extrapolation / ppg_ratio_range) * skill_value_range
334
+ new_skill_value = level.skill_value + skill_value_diff
335
+ weighted_skill_values.append((new_skill_value, correlation.n_games))
336
+ # if target_level.id == 229:
337
+ # print(f"Debug: From Level ID: {level.id}, To Level ID: {target_level.id}")
338
+ # print(f"Debug: From Skill Value: {level.skill_value} PPG Ratio: {ppg_ratio_for_extrapolation}, PPG Ratio Range: {ppg_ratio_range}")
339
+ # print(f"Debug: Skill Value Range: {skill_value_range}, Skill Value Diff: {skill_value_diff}")
340
+ # print(f"Debug: New Skill Value: {new_skill_value}")
341
+
342
+ # Calculate weighted average of new skill values
343
+ total_n_games = sum(n_games for _, n_games in weighted_skill_values)
344
+ weighted_avg_skill_value = sum(skill_value * n_games for skill_value, n_games in weighted_skill_values) / total_n_games
345
+ suggested_skill_values[target_level_id].append(weighted_avg_skill_value)
346
+
347
+ # Update skill values for target levels
348
+ for target_level_id, skill_values in suggested_skill_values.items():
349
+ skill_values = Config.discard_outliers(np.array(skill_values))
350
+ if len(skill_values) > 0:
351
+ avg_skill_value = float(sum(skill_values) / len(skill_values))
352
+ avg_skill_value = max(avg_skill_value, 9.6)
353
+ if avg_skill_value < min_skill_value:
354
+ avg_skill_value = min_skill_value - 0.01
355
+ session.query(Level).filter_by(id=target_level_id).update({
356
+ 'skill_value': avg_skill_value,
357
+ 'skill_propagation_sequence': propagation_sequence + 1
358
+ })
359
+ session.commit()
360
+
361
+ print(f"Skill levels have been propagated for sequence {propagation_sequence}.")
362
+
363
+ if __name__ == "__main__":
364
+ reset_skill_values_in_divisions()
365
+ build_levels_graph_edges()
366
+
367
+ for sequence in range(Config.MAX_PROPAGATION_SEQUENCE + 1):
368
+ propagate_skill_levels(sequence)
@@ -222,6 +222,20 @@ class DivisionStatsSkater(BaseStatsSkater):
222
222
  def get_aggregation_column(cls):
223
223
  return 'division_id'
224
224
 
225
+ class LevelStatsSkater(BaseStatsSkater):
226
+ __tablename__ = 'level_stats_skater'
227
+ level_id = db.Column(db.Integer, db.ForeignKey('levels.id'), nullable=False)
228
+ aggregation_id = synonym('level_id')
229
+
230
+ @declared_attr
231
+ def aggregation_type(cls):
232
+ return 'level'
233
+
234
+ @classmethod
235
+ def get_aggregation_column(cls):
236
+ return 'level_id'
237
+
238
+
225
239
  class OrgStatsGoalie(BaseStatsGoalie):
226
240
  __tablename__ = 'org_stats_goalie'
227
241
  org_id = db.Column(db.Integer, db.ForeignKey('organizations.id'), nullable=False)
@@ -561,3 +575,40 @@ class DivisionStatsWeeklyScorekeeper(BaseStatsScorekeeper):
561
575
  @classmethod
562
576
  def get_aggregation_column(cls):
563
577
  return 'division_id'
578
+
579
+ class LevelsGraphEdge(db.Model):
580
+ __tablename__ = 'levels_graph_edges'
581
+ id = db.Column(db.Integer, primary_key=True)
582
+ from_level_id = db.Column(db.Integer, db.ForeignKey('levels.id'), nullable=False)
583
+ to_level_id = db.Column(db.Integer, db.ForeignKey('levels.id'), nullable=False)
584
+ n_connections = db.Column(db.Integer, nullable=False)
585
+ ppg_ratio = db.Column(db.Float, nullable=False)
586
+ n_games = db.Column(db.Integer, nullable=False) # New field to store the number of games
587
+
588
+ __table_args__ = (
589
+ db.UniqueConstraint('from_level_id', 'to_level_id', name='_from_to_level_uc'),
590
+ )
591
+
592
+ class SkillPropagationCorrelation(db.Model):
593
+ __tablename__ = 'skill_propagation_correlation'
594
+ id = db.Column(db.Integer, primary_key=True)
595
+ skill_value_from = db.Column(db.Float, nullable=False)
596
+ skill_value_to = db.Column(db.Float, nullable=False)
597
+ ppg_ratio = db.Column(db.Float, nullable=False)
598
+
599
+ __table_args__ = (
600
+ db.UniqueConstraint('skill_value_from', 'skill_value_to', 'ppg_ratio', name='_skill_value_ppg_ratio_uc'),
601
+ )
602
+
603
+ # How PPG changes with INCREASING SKILL VALUES
604
+ class SkillValuePPGRatio(db.Model):
605
+ __tablename__ = 'skill_value_ppg_ratios'
606
+ id = db.Column(db.Integer, primary_key=True)
607
+ from_skill_value = db.Column(db.Float, nullable=False)
608
+ to_skill_value = db.Column(db.Float, nullable=False)
609
+ ppg_ratio = db.Column(db.Float, nullable=False)
610
+ n_games = db.Column(db.Integer, nullable=False) # New field to store the sum of games
611
+
612
+ __table_args__ = (
613
+ db.UniqueConstraint('from_skill_value', 'to_skill_value', name='_from_to_skill_value_uc'),
614
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hockey-blast-common-lib
3
- Version: 0.1.28
3
+ Version: 0.1.31
4
4
  Summary: Common library for shared functionality and DB models
5
5
  Author: Pavel Kletskov
6
6
  Author-email: kletskov@gmail.com
@@ -6,6 +6,7 @@ hockey_blast_common_lib/aggregate_goalie_stats.py
6
6
  hockey_blast_common_lib/aggregate_human_stats.py
7
7
  hockey_blast_common_lib/aggregate_referee_stats.py
8
8
  hockey_blast_common_lib/aggregate_skater_stats.py
9
+ hockey_blast_common_lib/assign_skater_skill.py
9
10
  hockey_blast_common_lib/db_connection.py
10
11
  hockey_blast_common_lib/dump_sample_db.sh
11
12
  hockey_blast_common_lib/hockey_blast_sample_backup.sql.gz
@@ -13,6 +14,7 @@ hockey_blast_common_lib/models.py
13
14
  hockey_blast_common_lib/options.py
14
15
  hockey_blast_common_lib/restore_sample_db.sh
15
16
  hockey_blast_common_lib/skills_in_divisions.py
17
+ hockey_blast_common_lib/skills_propagation.py
16
18
  hockey_blast_common_lib/stats_models.py
17
19
  hockey_blast_common_lib/utils.py
18
20
  hockey_blast_common_lib/wsgi.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='hockey-blast-common-lib', # The name of your package
5
- version='0.1.28',
5
+ version='0.1.31',
6
6
  description='Common library for shared functionality and DB models',
7
7
  author='Pavel Kletskov',
8
8
  author_email='kletskov@gmail.com',