hockey-blast-common-lib 0.1.28__tar.gz → 0.1.29__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 (26) hide show
  1. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/PKG-INFO +1 -1
  2. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/aggregate_skater_stats.py +39 -17
  3. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/models.py +5 -5
  4. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/options.py +1 -0
  5. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/skills_in_divisions.py +46 -35
  6. hockey_blast_common_lib-0.1.29/hockey_blast_common_lib/skills_propagation.py +251 -0
  7. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/stats_models.py +37 -0
  8. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib.egg-info/PKG-INFO +1 -1
  9. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib.egg-info/SOURCES.txt +1 -0
  10. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/setup.py +1 -1
  11. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/MANIFEST.in +0 -0
  12. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/README.md +0 -0
  13. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/__init__.py +0 -0
  14. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/aggregate_goalie_stats.py +0 -0
  15. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/aggregate_human_stats.py +0 -0
  16. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/aggregate_referee_stats.py +0 -0
  17. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/db_connection.py +0 -0
  18. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/dump_sample_db.sh +0 -0
  19. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/hockey_blast_sample_backup.sql.gz +0 -0
  20. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/restore_sample_db.sh +0 -0
  21. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/utils.py +0 -0
  22. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/wsgi.py +0 -0
  23. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib.egg-info/dependency_links.txt +0 -0
  24. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib.egg-info/requires.txt +0 -0
  25. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib.egg-info/top_level.txt +0 -0
  26. {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/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.29
4
4
  Summary: Common library for shared functionality and DB models
5
5
  Author: Pavel Kletskov
6
6
  Author-email: kletskov@gmail.com
@@ -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.")
@@ -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 = 10
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):
@@ -56,56 +56,67 @@ def reset_skill_values_in_divisions():
56
56
  for division in divisions:
57
57
  # Look up the Skill table using the level from Division
58
58
  div_level = division.level
59
-
60
59
  # 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()
60
+ level = session.query(Level).filter(Level.org_id == division.org_id, Level.level_name == div_level).one_or_none()
62
61
 
63
- if not skill:
62
+ if not level:
64
63
  # If no match found, check each alternative name individually
65
- skills = session.query(Skill).filter(Skill.org_id == division.org_id).all()
64
+ skills = session.query(Level).filter(Level.org_id == division.org_id).all()
66
65
  for s in skills:
67
66
  alternative_names = s.level_alternative_name.split(',')
68
67
  if div_level in alternative_names:
69
- skill = s
68
+ level = s
70
69
  break
71
70
 
72
- if skill:
71
+ if level:
73
72
  # 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}")
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
77
79
  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")
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}")
82
93
 
83
94
  # Commit the changes to the Division
84
95
  session.commit()
85
96
 
86
- print("Skill values and propagation sequences have been populated into the Division table.")
97
+ print("Level values and propagation sequences have been populated into the Division table.")
87
98
 
88
99
  def fill_seed_skills():
89
100
  session = create_session("boss")
90
101
 
91
102
  # List of Skill objects based on the provided comments
92
103
  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')
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')
109
120
  ]
110
121
 
111
122
  for skill in skills:
@@ -116,9 +127,9 @@ def fill_seed_skills():
116
127
 
117
128
  def get_fake_skill(session):
118
129
  # 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()
130
+ fake_skill = session.query(Level).filter_by(org_id=1, level_name='Fake Skill').first()
120
131
  if not fake_skill:
121
- fake_skill = Skill(
132
+ fake_skill = Level(
122
133
  org_id=1,
123
134
  skill_value=-1,
124
135
  level_name='Fake Skill',
@@ -143,7 +154,7 @@ def delete_all_skills():
143
154
  fake_skill = get_fake_skill(session)
144
155
  assign_fake_skill_to_divisions(session, fake_skill)
145
156
  # Delete all Skill records except the fake skill
146
- session.query(Skill).filter(Skill.id != fake_skill.id).delete(synchronize_session=False)
157
+ session.query(Level).filter(Level.id != fake_skill.id).delete(synchronize_session=False)
147
158
  session.commit()
148
159
  print("All Skill records except the fake skill have been deleted.")
149
160
 
@@ -177,7 +188,7 @@ def populate_league_ids():
177
188
 
178
189
  if __name__ == "__main__":
179
190
  # delete_all_skills()
180
- # fill_seed_skills()
191
+ #fill_seed_skills()
181
192
  reset_skill_values_in_divisions()
182
193
  #populate_season_ids() # Call the function to populate season_ids
183
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.28
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
@@ -13,6 +13,7 @@ hockey_blast_common_lib/models.py
13
13
  hockey_blast_common_lib/options.py
14
14
  hockey_blast_common_lib/restore_sample_db.sh
15
15
  hockey_blast_common_lib/skills_in_divisions.py
16
+ hockey_blast_common_lib/skills_propagation.py
16
17
  hockey_blast_common_lib/stats_models.py
17
18
  hockey_blast_common_lib/utils.py
18
19
  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.29',
6
6
  description='Common library for shared functionality and DB models',
7
7
  author='Pavel Kletskov',
8
8
  author_email='kletskov@gmail.com',