hockey-blast-common-lib 0.1.27__py3-none-any.whl → 0.1.29__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.
@@ -6,12 +6,12 @@ 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
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
16
 
17
17
  def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_filter_out, filter_human_id=None, aggregation_window=None):
@@ -35,6 +35,10 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
35
35
  StatsModel = DivisionStatsSkater
36
36
  min_games = MIN_GAMES_FOR_DIVISION_STATS
37
37
  filter_condition = Game.division_id == aggregation_id
38
+ elif aggregation_type == 'level':
39
+ StatsModel = LevelStatsSkater
40
+ min_games = MIN_GAMES_FOR_LEVEL_STATS
41
+ filter_condition = Division.level_id == aggregation_id
38
42
  else:
39
43
  raise ValueError("Invalid aggregation type")
40
44
 
@@ -307,20 +311,38 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, names_to_f
307
311
  print(f"\r{total_items}/{total_items} (100.00%)")
308
312
  print("\nDone.")
309
313
 
310
- # Example usage
311
314
  if __name__ == "__main__":
312
- args = parse_args()
313
- org_alias = args.org
314
315
  session = create_session("boss")
315
- org_id = get_org_id_from_alias(session, org_alias)
316
316
 
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')
317
+ # Get all org_id present in the Organization table
318
+ org_ids = session.query(Organization.id).all()
319
+ org_ids = [org_id[0] for org_id in org_ids]
323
320
 
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')
321
+ # for org_id in org_ids:
322
+ # division_ids = get_all_division_ids_for_org(session, org_id)
323
+ # print(f"Aggregating skater stats for {len(division_ids)} divisions in org_id {org_id}...")
324
+ # total_divisions = len(division_ids)
325
+ # processed_divisions = 0
326
+ # for division_id in division_ids:
327
+ # aggregate_skater_stats(session, aggregation_type='division', aggregation_id=division_id, names_to_filter_out=not_human_names, filter_human_id=None)
328
+ # 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')
329
+ # 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')
330
+ # processed_divisions += 1
331
+ # print(f"\rProcessed {processed_divisions}/{total_divisions} divisions ({(processed_divisions/total_divisions)*100:.2f}%)", end="")
332
+
333
+ # aggregate_skater_stats(session, aggregation_type='org', aggregation_id=org_id, names_to_filter_out=not_human_names, filter_human_id=None)
334
+ # 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')
335
+ # 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')
336
+
337
+ # Aggregate by level
338
+ level_ids = session.query(Division.level_id).distinct().all()
339
+ level_ids = [level_id[0] for level_id in level_ids]
340
+ total_levels = len(level_ids)
341
+ processed_levels = 0
342
+ for level_id in level_ids:
343
+ if level_id is None:
344
+ continue
345
+ print(f"\rProcessed {processed_levels}/{total_levels} levels ({(processed_levels/total_levels)*100:.2f}%)", end="")
346
+ processed_levels += 1
347
+ aggregate_skater_stats(session, aggregation_type='level', aggregation_id=level_id, names_to_filter_out=not_human_names, filter_human_id=None)
348
+ print("\nDone.")
@@ -15,10 +15,11 @@ class Comment(db.Model):
15
15
  class Division(db.Model):
16
16
  __tablename__ = 'divisions'
17
17
  id = db.Column(db.Integer, primary_key=True)
18
- league_number = db.Column(db.Integer)
19
- season_number = db.Column(db.Integer)
20
- level = db.Column(db.String(100)) # UNIQUE LEVEL NAME
21
- skill_id = db.Column(db.Integer, db.ForeignKey('skills.id')) # SKILL LEVEL
18
+ league_number = db.Column(db.Integer) # TODO: Deprecate usage and remove (get this info through Season->League)
19
+ season_number = db.Column(db.Integer) # TODO: Deprecate usage and remove (get this info from Season by season_id)
20
+ season_id = db.Column(db.Integer, db.ForeignKey('seasons.id'))
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
22
23
  org_id = db.Column(db.Integer, db.ForeignKey('organizations.id'), nullable=False)
23
24
  __table_args__ = (
24
25
  db.UniqueConstraint('org_id', 'league_number', 'season_number', 'level', name='_org_league_season_level_uc'),
@@ -152,14 +153,15 @@ class League(db.Model):
152
153
  db.UniqueConstraint('org_id', 'league_number', name='_org_league_number_uc'),
153
154
  )
154
155
 
155
- class Skill(db.Model):
156
- __tablename__ = 'skills'
156
+ class Level(db.Model):
157
+ __tablename__ = 'levels'
157
158
  id = db.Column(db.Integer, primary_key=True)
158
159
  org_id = db.Column(db.Integer, db.ForeignKey('organizations.id'), nullable=False)
159
160
  skill_value = db.Column(db.Float) # A number from 0 (NHL) to 100 (pedestrian)
160
161
  level_name = db.Column(db.String(100), unique=True)
161
162
  level_alternative_name = db.Column(db.String(100))
162
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)
163
165
  __table_args__ = (
164
166
  db.UniqueConstraint('org_id', 'level_name', name='_org_level_name_uc'),
165
167
  )
@@ -295,7 +297,8 @@ class Season(db.Model):
295
297
  season_name = db.Column(db.String(100))
296
298
  start_date = db.Column(db.Date)
297
299
  end_date = db.Column(db.Date)
298
- league_number = db.Column(db.Integer)
300
+ league_number = db.Column(db.Integer) # TODO: Deprecate usage and remove (get this info from League by league_id)
301
+ league_id = db.Column(db.Integer, db.ForeignKey('leagues.id'))
299
302
  org_id = db.Column(db.Integer, db.ForeignKey('organizations.id'), nullable=False)
300
303
  __table_args__ = (
301
304
  db.UniqueConstraint('org_id', 'league_number', 'season_number', name='_org_league_season_uc'),
@@ -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 = 10
7
8
 
8
9
  orgs = {'caha', 'sharksice', 'tvice'}
9
10
 
@@ -0,0 +1,194 @@
1
+ import sys
2
+ import os
3
+ from collections import defaultdict
4
+
5
+ # Add the project root directory to the Python path
6
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
7
+
8
+ from hockey_blast_common_lib.models import Game, Division, Level, Season, League
9
+ from hockey_blast_common_lib.db_connection import create_session
10
+
11
+ def analyze_levels(org):
12
+ session = create_session(org)
13
+
14
+ # Query to get games and their divisions for season_number 33 and 35 with league_number 1
15
+ games_season_33 = session.query(Game.home_team_id, Game.visitor_team_id, Division.level).join(Division, Game.division_id == Division.id).filter(Division.season_number == 33, Division.league_number == 1).all()
16
+ games_season_35 = session.query(Game.home_team_id, Game.visitor_team_id, Division.level).join(Division, Game.division_id == Division.id).filter(Division.season_number == 35, Division.league_number == 1).all()
17
+
18
+ # Dictionary to store levels for each team by season
19
+ team_levels_season_33 = defaultdict(set)
20
+ team_levels_season_35 = defaultdict(set)
21
+
22
+ # Populate the dictionaries
23
+ for home_team_id, visitor_team_id, level in games_season_33:
24
+ team_levels_season_33[home_team_id].add(level)
25
+ team_levels_season_33[visitor_team_id].add(level)
26
+
27
+ for home_team_id, visitor_team_id, level in games_season_35:
28
+ team_levels_season_35[home_team_id].add(level)
29
+ team_levels_season_35[visitor_team_id].add(level)
30
+
31
+ # Dictionary to store level name connections
32
+ level_connections = defaultdict(lambda: defaultdict(int))
33
+
34
+ # Analyze the level name connections
35
+ for team_id in team_levels_season_33:
36
+ if team_id in team_levels_season_35:
37
+ for old_level in team_levels_season_33[team_id]:
38
+ for new_level in team_levels_season_35[team_id]:
39
+ level_connections[new_level][old_level] += 1
40
+
41
+ # Output the results
42
+ for new_level in sorted(level_connections.keys()):
43
+ connections = level_connections[new_level]
44
+ connections_list = sorted(connections.items(), key=lambda x: x[0])
45
+ connections_str = ", ".join([f"{old_level}: {count}" for old_level, count in connections_list])
46
+ print(f"{new_level}: {connections_str}")
47
+
48
+ session.close()
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
+ # Query to find the matching Skill
60
+ level = session.query(Level).filter(Level.org_id == division.org_id, Level.level_name == div_level).one_or_none()
61
+
62
+ if not level:
63
+ # If no match found, check each alternative name individually
64
+ skills = session.query(Level).filter(Level.org_id == division.org_id).all()
65
+ for s in skills:
66
+ alternative_names = s.level_alternative_name.split(',')
67
+ if div_level in alternative_names:
68
+ level = s
69
+ break
70
+
71
+ if level:
72
+ # Assign the skill_value and set skill_propagation_sequence to 0
73
+ division.level_id = level.id
74
+ if level.is_seed:
75
+ level.skill_propagation_sequence = 0
76
+ else:
77
+ level.skill_propagation_sequence = -1
78
+ level.skill_value = -1
79
+ else:
80
+ # Add new Skill with values previously used for division
81
+ new_level = Level(
82
+ org_id=division.org_id,
83
+ skill_value=-1,
84
+ level_name=division.level,
85
+ level_alternative_name='',
86
+ is_seed=False,
87
+ skill_propagation_sequence=-1
88
+ )
89
+ session.add(new_level)
90
+ session.commit()
91
+ division.skill_id = new_level.id
92
+ print(f"Created new Level for Division {division.level}")
93
+
94
+ # Commit the changes to the Division
95
+ session.commit()
96
+
97
+ print("Level values and propagation sequences have been populated into the Division table.")
98
+
99
+ def fill_seed_skills():
100
+ session = create_session("boss")
101
+
102
+ # List of Skill objects based on the provided comments
103
+ skills = [
104
+ Level(is_seed=True, org_id=1, skill_value=10.0, level_name='Adult Division 1', level_alternative_name='Senior A'),
105
+ Level(is_seed=True, org_id=1, skill_value=20.0, level_name='Adult Division 2', level_alternative_name='Senior B'),
106
+ Level(is_seed=True, org_id=1, skill_value=30.0, level_name='Adult Division 3A', level_alternative_name='Senior BB'),
107
+ Level(is_seed=True, org_id=1, skill_value=35.0, level_name='Adult Division 3B', level_alternative_name='Senior C'),
108
+ Level(is_seed=True, org_id=1, skill_value=40.0, level_name='Adult Division 4A', level_alternative_name='Senior CC'),
109
+ Level(is_seed=True, org_id=1, skill_value=45.0, level_name='Adult Division 4B', level_alternative_name='Senior CCC,Senior CCCC'),
110
+ Level(is_seed=True, org_id=1, skill_value=50.0, level_name='Adult Division 5A', level_alternative_name='Senior D,Senior DD'),
111
+ Level(is_seed=True, org_id=1, skill_value=55.0, level_name='Adult Division 5B', level_alternative_name='Senior DDD'),
112
+ Level(is_seed=True, org_id=1, skill_value=60.0, level_name='Adult Division 6A', level_alternative_name='Senior DDDD'),
113
+ Level(is_seed=True, org_id=1, skill_value=65.0, level_name='Adult Division 6B', level_alternative_name='Senior DDDDD'),
114
+ Level(is_seed=True, org_id=1, skill_value=70.0, level_name='Adult Division 7A', level_alternative_name='Senior E'),
115
+ Level(is_seed=True, org_id=1, skill_value=75.0, level_name='Adult Division 7B', level_alternative_name='Senior EE'),
116
+ Level(is_seed=True, org_id=1, skill_value=80.0, level_name='Adult Division 8', level_alternative_name='Senior EEE'),
117
+ Level(is_seed=True, org_id=1, skill_value=80.0, level_name='Adult Division 8A', level_alternative_name='Senior EEE'),
118
+ Level(is_seed=True, org_id=1, skill_value=85.0, level_name='Adult Division 8B', level_alternative_name='Senior EEEE'),
119
+ Level(is_seed=True, org_id=1, skill_value=90.0, level_name='Adult Division 9', level_alternative_name='Senior EEEEE')
120
+ ]
121
+
122
+ for skill in skills:
123
+ session.add(skill)
124
+ session.commit()
125
+
126
+ print("Seed skills have been populated into the database.")
127
+
128
+ def get_fake_skill(session):
129
+ # Create a special fake Skill with org_id == -1 and skill_value == -1
130
+ fake_skill = session.query(Level).filter_by(org_id=1, level_name='Fake Skill').first()
131
+ if not fake_skill:
132
+ fake_skill = Level(
133
+ org_id=1,
134
+ skill_value=-1,
135
+ level_name='Fake Skill',
136
+ level_alternative_name='',
137
+ is_seed=False
138
+ )
139
+ session.add(fake_skill)
140
+ session.commit()
141
+ print("Created special fake Skill record.")
142
+ return fake_skill
143
+
144
+ def assign_fake_skill_to_divisions(session, fake_skill):
145
+ # Assign the special fake Skill to every existing Division
146
+ divisions = session.query(Division).all()
147
+ for division in divisions:
148
+ division.skill_id = fake_skill.id
149
+ session.commit()
150
+ print("Assigned special fake Skill to all Division records.")
151
+
152
+ def delete_all_skills():
153
+ session = create_session("boss")
154
+ fake_skill = get_fake_skill(session)
155
+ assign_fake_skill_to_divisions(session, fake_skill)
156
+ # Delete all Skill records except the fake skill
157
+ session.query(Level).filter(Level.id != fake_skill.id).delete(synchronize_session=False)
158
+ session.commit()
159
+ print("All Skill records except the fake skill have been deleted.")
160
+
161
+ def populate_season_ids():
162
+ session = create_session("boss")
163
+ divisions = session.query(Division).all()
164
+ for division in divisions:
165
+ # Find the Season record that matches the season_number
166
+ season = session.query(Season).filter_by(season_number=division.season_number, org_id=division.org_id, league_number=division.league_number).first()
167
+ if season:
168
+ division.season_id = season.id
169
+ print(f"Assigned season_id {season.id} for Division with season_number {division.season_number}")
170
+ else:
171
+ print(f"Season not found for Division with season_number {division.season_number}")
172
+ session.commit()
173
+ print("Season IDs have been populated into the Division table.")
174
+
175
+ def populate_league_ids():
176
+ session = create_session("boss")
177
+ seasons = session.query(Season).all()
178
+ for season in seasons:
179
+ # Find the League record that matches the league_number and org_id
180
+ league = session.query(League).filter_by(league_number=season.league_number, org_id=season.org_id).first()
181
+ if league:
182
+ season.league_id = league.id
183
+ print(f"Assigned league_id {league.id} for Season with league_number {season.league_number}")
184
+ else:
185
+ print(f"League not found for Season with league_number {season.league_number}")
186
+ session.commit()
187
+ print("League IDs have been populated into the Season table.")
188
+
189
+ if __name__ == "__main__":
190
+ # delete_all_skills()
191
+ #fill_seed_skills()
192
+ reset_skill_values_in_divisions()
193
+ #populate_season_ids() # Call the function to populate season_ids
194
+ #populate_league_ids() # Call the new function to populate league_ids
@@ -0,0 +1,251 @@
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, Season
10
+ from hockey_blast_common_lib.stats_models import LevelsGraphEdge, LevelStatsSkater, SkillPropagationCorrelation
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 = 10
18
+ MIN_PPG = 0.3
19
+ MIN_HUMANS_FOR_EDGE = 5
20
+ MAX_START_DATE_DIFF_MONTHS = 15
21
+ MAX_PROPAGATION_SEQUENCE = 0
22
+ MIN_CONNECTIONS_FOR_CORRELATION = 40
23
+ MIN_CONNECTIONS_FOR_PROPAGATION = 3
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
+
44
+ def build_levels_graph_edges():
45
+ session = create_session("boss")
46
+
47
+ # Delete all existing edges
48
+ session.query(LevelsGraphEdge).delete()
49
+ session.commit()
50
+
51
+ # Query to get all level stats
52
+ level_stats = session.query(LevelStatsSkater).all()
53
+
54
+ # Dictionary to store stats by level and human
55
+ level_human_stats = defaultdict(lambda: defaultdict(dict))
56
+
57
+ for stat in level_stats:
58
+ if stat.games_played >= Config.MIN_GAMES_PLAYED and stat.points_per_game >= Config.MIN_PPG:
59
+ level_human_stats[stat.aggregation_id][stat.human_id] = {
60
+ 'games_played': stat.games_played,
61
+ 'points_per_game': stat.points_per_game
62
+ }
63
+
64
+ # Dictionary to store edges
65
+ edges = {}
66
+
67
+ # Build edges
68
+ total_levels = len(level_human_stats)
69
+ processed_levels = 0
70
+ for from_level_id, from_humans in level_human_stats.items():
71
+ from_level = session.query(Level).filter_by(id=from_level_id).first()
72
+ from_season = session.query(Season).filter_by(id=from_level.season_id).first()
73
+ for to_level_id, to_humans in level_human_stats.items():
74
+ to_level = session.query(Level).filter_by(id=to_level_id).first()
75
+ to_season = session.query(Season).filter_by(id=to_level.season_id).first()
76
+
77
+ if from_level.skill_value >= to_level.skill_value:
78
+ continue
79
+
80
+ # TMP DEBUG HACK
81
+ if from_level.skill_value != 10 and to_level.skill_value != 30:
82
+ continue
83
+
84
+ # Check if the start dates are within the allowed difference
85
+ if abs((from_season.start_date - to_season.start_date).days) > Config.MAX_START_DATE_DIFF_MONTHS * 30:
86
+ continue
87
+
88
+ common_humans = set(from_humans.keys()) & set(to_humans.keys())
89
+ n_connections = len(common_humans)
90
+
91
+ if n_connections < Config.MIN_HUMANS_FOR_EDGE:
92
+ continue
93
+
94
+ ppg_ratios = []
95
+ for human_id in common_humans:
96
+ from_ppg = from_humans[human_id]['points_per_game']
97
+ to_ppg = to_humans[human_id]['points_per_game']
98
+ if from_level.skill_value == 10 and to_level.skill_value == 30:
99
+ print(f"Human {human_id} From PPG: {from_ppg}, To PPG: {to_ppg}")
100
+ if from_ppg > 0 and to_ppg > 0:
101
+ ppg_ratios.append(to_ppg / from_ppg)
102
+
103
+ if not ppg_ratios:
104
+ continue
105
+
106
+ # Discard outliers
107
+ ppg_ratios = Config.discard_outliers(np.array(ppg_ratios))
108
+
109
+ if len(ppg_ratios) == 0:
110
+ continue
111
+
112
+ avg_ppg_ratio = float(sum(ppg_ratios) / len(ppg_ratios))
113
+ if avg_ppg_ratio < 1.0:
114
+ avg_ppg_ratio = 1 / avg_ppg_ratio
115
+ from_level_id, to_level_id = to_level_id, from_level_id
116
+
117
+ edge = LevelsGraphEdge(
118
+ from_level_id=from_level_id,
119
+ to_level_id=to_level_id,
120
+ n_connections=n_connections,
121
+ ppg_ratio=avg_ppg_ratio
122
+ )
123
+ edges[(from_level_id, to_level_id)] = edge
124
+
125
+ processed_levels += 1
126
+ print(f"\rProcessed {processed_levels}/{total_levels} levels ({(processed_levels/total_levels)*100:.2f}%)", end="")
127
+
128
+ # Insert edges into the database
129
+ for edge in edges.values():
130
+ session.add(edge)
131
+ session.commit()
132
+
133
+ print("\nLevels graph edges have been populated into the database.")
134
+
135
+ def propagate_skill_levels(propagation_sequence):
136
+ session = create_session("boss")
137
+
138
+ if propagation_sequence == 0:
139
+ # Delete all existing correlation data
140
+ session.query(SkillPropagationCorrelation).delete()
141
+ session.commit()
142
+
143
+ # Build and save the correlation data
144
+ levels = session.query(Level).filter(Level.skill_propagation_sequence == 0).all()
145
+ level_ids = {level.id for level in levels}
146
+ correlation_data = defaultdict(list)
147
+
148
+ for level in levels:
149
+ if level.skill_value == -1:
150
+ continue
151
+
152
+ edges = session.query(LevelsGraphEdge).filter(
153
+ (LevelsGraphEdge.from_level_id == level.id) |
154
+ (LevelsGraphEdge.to_level_id == level.id)
155
+ ).all()
156
+
157
+ for edge in edges:
158
+ if edge.n_connections < Config.MIN_CONNECTIONS_FOR_CORRELATION:
159
+ continue
160
+
161
+ if edge.from_level_id == level.id:
162
+ target_level_id = edge.to_level_id
163
+ ppg_ratio = edge.ppg_ratio
164
+ else:
165
+ target_level_id = edge.from_level_id
166
+ ppg_ratio = 1 / edge.ppg_ratio
167
+
168
+ if target_level_id not in level_ids:
169
+ continue
170
+
171
+ target_level = session.query(Level).filter_by(id=target_level_id).first()
172
+ if target_level:
173
+ skill_value_from = level.skill_value
174
+ skill_value_to = target_level.skill_value
175
+
176
+ # Since we go over all levels in the sequence 0, we will see each edge twice
177
+ # This condition eliminates duplicates
178
+ if skill_value_from >= skill_value_to:
179
+ continue
180
+
181
+ # Debug prints
182
+ print(f"From Skill {level.skill_value} to {target_level.skill_value} ratio: {ppg_ratio}")
183
+
184
+ correlation_data[(skill_value_from, skill_value_to)].append(
185
+ ppg_ratio
186
+ )
187
+
188
+ # Save correlation data to the database
189
+ for (skill_value_from, skill_value_to), ppg_ratios in correlation_data.items():
190
+ ppg_ratios = Config.discard_outliers(np.array(ppg_ratios))
191
+ if len(ppg_ratios) > 0:
192
+ avg_ppg_ratio = float(sum(ppg_ratios) / len(ppg_ratios))
193
+ correlation = SkillPropagationCorrelation(
194
+ skill_value_from=skill_value_from,
195
+ skill_value_to=skill_value_to,
196
+ ppg_ratio=avg_ppg_ratio
197
+ )
198
+ session.add(correlation)
199
+ session.commit()
200
+
201
+ return
202
+ # Propagate skill levels
203
+ levels = session.query(Level).filter(Level.skill_propagation_sequence == propagation_sequence).all()
204
+ suggested_skill_values = defaultdict(list)
205
+
206
+ for level in levels:
207
+ edges = session.query(LevelsGraphEdge).filter(
208
+ (LevelsGraphEdge.from_level_id == level.id) |
209
+ (LevelsGraphEdge.to_level_id == level.id)
210
+ ).all()
211
+
212
+ for edge in edges:
213
+ if edge.n_connections < Config.MIN_CONNECTIONS_FOR_PROPAGATION:
214
+ continue
215
+
216
+ if edge.from_level_id == level.id:
217
+ target_level_id = edge.to_level_id
218
+ ppg_ratio = edge.ppg_ratio
219
+ else:
220
+ target_level_id = edge.from_level_id
221
+ ppg_ratio = 1 / edge.ppg_ratio
222
+
223
+ target_level = session.query(Level).filter_by(id=target_level_id).first()
224
+ if target_level and target_level.skill_propagation_sequence == -1:
225
+ correlation = session.query(SkillPropagationCorrelation).filter_by(
226
+ skill_value_from=min(level.skill_value, target_level.skill_value),
227
+ skill_value_to=max(level.skill_value, target_level.skill_value),
228
+ ppg_ratio=ppg_ratio if level.skill_value < target_level.skill_value else 1 / ppg_ratio
229
+ ).first()
230
+
231
+ if correlation:
232
+ suggested_skill_values[target_level_id].append(correlation.skill_value_to)
233
+
234
+ # Update skill values for target levels
235
+ for target_level_id, skill_values in suggested_skill_values.items():
236
+ skill_values = Config.discard_outliers(np.array(skill_values))
237
+ if len(skill_values) > 0:
238
+ avg_skill_value = float(sum(skill_values) / len(skill_values))
239
+ session.query(Level).filter_by(id=target_level_id).update({
240
+ 'skill_value': avg_skill_value,
241
+ 'skill_propagation_sequence': propagation_sequence + 1
242
+ })
243
+ session.commit()
244
+
245
+ print(f"Skill levels have been propagated for sequence {propagation_sequence}.")
246
+
247
+ if __name__ == "__main__":
248
+ build_levels_graph_edges()
249
+
250
+ for sequence in range(Config.MAX_PROPAGATION_SEQUENCE + 1):
251
+ 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,26 @@ 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
+
587
+ __table_args__ = (
588
+ db.UniqueConstraint('from_level_id', 'to_level_id', name='_from_to_level_uc'),
589
+ )
590
+
591
+ class SkillPropagationCorrelation(db.Model):
592
+ __tablename__ = 'skill_propagation_correlation'
593
+ id = db.Column(db.Integer, primary_key=True)
594
+ skill_value_from = db.Column(db.Float, nullable=False)
595
+ skill_value_to = db.Column(db.Float, nullable=False)
596
+ ppg_ratio = db.Column(db.Float, nullable=False)
597
+
598
+ __table_args__ = (
599
+ db.UniqueConstraint('skill_value_from', 'skill_value_to', 'ppg_ratio', name='_skill_value_ppg_ratio_uc'),
600
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hockey-blast-common-lib
3
- Version: 0.1.27
3
+ Version: 0.1.29
4
4
  Summary: Common library for shared functionality and DB models
5
5
  Author: Pavel Kletskov
6
6
  Author-email: kletskov@gmail.com
@@ -2,17 +2,19 @@ hockey_blast_common_lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
2
2
  hockey_blast_common_lib/aggregate_goalie_stats.py,sha256=d2qav46Rg2DNIYRj_Ubj1kpQmoPUJHKiwEWOVU25nD4,8742
3
3
  hockey_blast_common_lib/aggregate_human_stats.py,sha256=88OMhTgQjzc9xIakf6kW9_lZwbSXkpsZy8C0pX-Wlq8,14229
4
4
  hockey_blast_common_lib/aggregate_referee_stats.py,sha256=A0PTyEbPUjqfXxlJCDOVioFaQk9AyjjhiWEuRuu35v0,11036
5
- hockey_blast_common_lib/aggregate_skater_stats.py,sha256=37fhgej9trukr8cGaK7DT1HoxBcp95qwsypYCCziqqc,15563
5
+ hockey_blast_common_lib/aggregate_skater_stats.py,sha256=H_RPz26erdvKXz6w8NjSfQAZgcpXitFKsuhmrYYqWyg,16839
6
6
  hockey_blast_common_lib/db_connection.py,sha256=HvPxDvOj7j5H85RfslGvHVNevfg7mKCd0syJ6NX21mU,1890
7
7
  hockey_blast_common_lib/dump_sample_db.sh,sha256=MHPA-Ciod7wsvAlMbRtXFiyajgnEqU1xR59sJQ9UWR0,738
8
- hockey_blast_common_lib/hockey_blast_sample_backup.sql.gz,sha256=UBbPuU8WzHmxIQScT1uXIbTpjWkkt3gu-FS2LNjBx2A,1033691
9
- hockey_blast_common_lib/models.py,sha256=3sZan04IpaDpUX6h84gHO_Qyf-fF32QMpcLXgR3cCfc,15472
10
- hockey_blast_common_lib/options.py,sha256=-LtEX8duw5Pl3CSpjFlLM5FPvrZuTAxTfSlDPa7H6mQ,761
8
+ hockey_blast_common_lib/hockey_blast_sample_backup.sql.gz,sha256=DcRvR7oyP2gbVU7fYUP8-Z9R79kl0jWt4zgFyNECstk,1033685
9
+ hockey_blast_common_lib/models.py,sha256=2YkFHgzvxLSU2-fURsyIqdaXjcQwZ_wjauYR9wWQtDw,15924
10
+ hockey_blast_common_lib/options.py,sha256=S545jnyw14GUiqDyVbwHlNlb3WStpJpsqmRqJ9-mBA0,792
11
11
  hockey_blast_common_lib/restore_sample_db.sh,sha256=u2zKazC6vNMULkpYzI64nlneCWaGUtDHPBAU-gWgRbw,1861
12
- hockey_blast_common_lib/stats_models.py,sha256=PI-mL1jmjCHLAvaATxSsjHEn05g9L_reA_YpsITPWjQ,21047
12
+ hockey_blast_common_lib/skills_in_divisions.py,sha256=BDoA9sGHH86chErTFLmK3oYMiZJwrfUtvKV7kf6pthg,9427
13
+ hockey_blast_common_lib/skills_propagation.py,sha256=tFUXETJJNvV_kq_JTs85LhbS8mRLHKa6ZpW9foYRfI8,9779
14
+ hockey_blast_common_lib/stats_models.py,sha256=W2NbVqQgUucbRsM0uA3tCy7ee7C4ElJ-UOlVDuhBFZM,22388
13
15
  hockey_blast_common_lib/utils.py,sha256=odDJWCK0BgbResXeoUzxbVChjaxcXr168ZxbrAw3L_8,3752
14
16
  hockey_blast_common_lib/wsgi.py,sha256=7LGUzioigviJp-EUhSEaQcd4jBae0mxbkyBscQfZhlc,730
15
- hockey_blast_common_lib-0.1.27.dist-info/METADATA,sha256=9McZBCPSrHLzYxACOj_c4ty-B-rDYqOnjswg_OYLJ1c,318
16
- hockey_blast_common_lib-0.1.27.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
17
- hockey_blast_common_lib-0.1.27.dist-info/top_level.txt,sha256=wIR4LIkE40npoA2QlOdfCYlgFeGbsHR8Z6r0h46Vtgc,24
18
- hockey_blast_common_lib-0.1.27.dist-info/RECORD,,
17
+ hockey_blast_common_lib-0.1.29.dist-info/METADATA,sha256=Dhg1VvioCggFoPw2GqvIP0DSOIU0VswxuKbxCs5IYQo,318
18
+ hockey_blast_common_lib-0.1.29.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
19
+ hockey_blast_common_lib-0.1.29.dist-info/top_level.txt,sha256=wIR4LIkE40npoA2QlOdfCYlgFeGbsHR8Z6r0h46Vtgc,24
20
+ hockey_blast_common_lib-0.1.29.dist-info/RECORD,,