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.
- {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/PKG-INFO +1 -1
- {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/aggregate_skater_stats.py +39 -17
- {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/models.py +5 -5
- {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/options.py +1 -0
- {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/skills_in_divisions.py +46 -35
- hockey_blast_common_lib-0.1.29/hockey_blast_common_lib/skills_propagation.py +251 -0
- {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/stats_models.py +37 -0
- {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib.egg-info/PKG-INFO +1 -1
- {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib.egg-info/SOURCES.txt +1 -0
- {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/setup.py +1 -1
- {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/MANIFEST.in +0 -0
- {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/README.md +0 -0
- {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/__init__.py +0 -0
- {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/aggregate_goalie_stats.py +0 -0
- {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/aggregate_human_stats.py +0 -0
- {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/aggregate_referee_stats.py +0 -0
- {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/db_connection.py +0 -0
- {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/dump_sample_db.sh +0 -0
- {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
- {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/restore_sample_db.sh +0 -0
- {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/utils.py +0 -0
- {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/wsgi.py +0 -0
- {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
- {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib.egg-info/requires.txt +0 -0
- {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
- {hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/setup.cfg +0 -0
@@ -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
|
-
|
318
|
-
|
319
|
-
for
|
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
|
-
|
325
|
-
|
326
|
-
|
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.")
|
{hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/models.py
RENAMED
@@ -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
|
-
|
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
|
158
|
-
__tablename__ = '
|
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
|
)
|
@@ -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,
|
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
|
-
|
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
|
62
|
+
if not level:
|
64
63
|
# If no match found, check each alternative name individually
|
65
|
-
skills = session.query(
|
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
|
-
|
68
|
+
level = s
|
70
69
|
break
|
71
70
|
|
72
|
-
if
|
71
|
+
if level:
|
73
72
|
# Assign the skill_value and set skill_propagation_sequence to 0
|
74
|
-
division.
|
75
|
-
|
76
|
-
|
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
|
-
#
|
79
|
-
|
80
|
-
|
81
|
-
|
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("
|
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
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
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(
|
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 =
|
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(
|
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
|
-
#
|
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
|
+
)
|
@@ -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.
|
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',
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/utils.py
RENAMED
File without changes
|
{hockey_blast_common_lib-0.1.28 → hockey_blast_common_lib-0.1.29}/hockey_blast_common_lib/wsgi.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|