spforge 0.8.29__py3-none-any.whl → 0.8.30__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.
Potentially problematic release.
This version of spforge might be problematic. Click here for more details.
- spforge/data_structures.py +4 -0
- spforge/ratings/_player_rating.py +114 -5
- {spforge-0.8.29.dist-info → spforge-0.8.30.dist-info}/METADATA +1 -1
- {spforge-0.8.29.dist-info → spforge-0.8.30.dist-info}/RECORD +8 -8
- tests/ratings/test_player_rating_generator.py +111 -0
- {spforge-0.8.29.dist-info → spforge-0.8.30.dist-info}/WHEEL +0 -0
- {spforge-0.8.29.dist-info → spforge-0.8.30.dist-info}/licenses/LICENSE +0 -0
- {spforge-0.8.29.dist-info → spforge-0.8.30.dist-info}/top_level.txt +0 -0
spforge/data_structures.py
CHANGED
|
@@ -12,6 +12,8 @@ class ColumnNames:
|
|
|
12
12
|
position: str | None = None
|
|
13
13
|
participation_weight: str | None = None
|
|
14
14
|
projected_participation_weight: str | None = None
|
|
15
|
+
defense_participation_weight: str | None = None
|
|
16
|
+
projected_defense_participation_weight: str | None = None
|
|
15
17
|
update_match_id: str | None = None
|
|
16
18
|
parent_team_id: str | None = None
|
|
17
19
|
team_players_playing_time: str | None = None
|
|
@@ -81,6 +83,8 @@ class MatchPerformance:
|
|
|
81
83
|
performance_value: float | None
|
|
82
84
|
participation_weight: float | None
|
|
83
85
|
projected_participation_weight: float
|
|
86
|
+
defense_participation_weight: float | None = None
|
|
87
|
+
projected_defense_participation_weight: float | None = None
|
|
84
88
|
team_players_playing_time: dict[str, float] | None = None
|
|
85
89
|
opponent_players_playing_time: dict[str, float] | None = None
|
|
86
90
|
|
|
@@ -39,6 +39,8 @@ from spforge.feature_generator._utils import to_polars
|
|
|
39
39
|
PLAYER_STATS = "__PLAYER_STATS"
|
|
40
40
|
_SCALED_PW = "__scaled_participation_weight__"
|
|
41
41
|
_SCALED_PPW = "__scaled_projected_participation_weight__"
|
|
42
|
+
_SCALED_DPW = "__scaled_defense_participation_weight__"
|
|
43
|
+
_SCALED_PDPW = "__scaled_projected_defense_participation_weight__"
|
|
42
44
|
|
|
43
45
|
|
|
44
46
|
class PlayerRatingGenerator(RatingGenerator):
|
|
@@ -186,6 +188,8 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
186
188
|
self.auto_scale_participation_weights = bool(auto_scale_participation_weights)
|
|
187
189
|
self._participation_weight_max: float | None = None
|
|
188
190
|
self._projected_participation_weight_max: float | None = None
|
|
191
|
+
self._defense_participation_weight_max: float | None = None
|
|
192
|
+
self._projected_defense_participation_weight_max: float | None = None
|
|
189
193
|
|
|
190
194
|
self._player_off_ratings: dict[str, PlayerRating] = {}
|
|
191
195
|
self._player_def_ratings: dict[str, PlayerRating] = {}
|
|
@@ -233,8 +237,11 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
233
237
|
eps = 1e-6
|
|
234
238
|
return min_val < -eps or max_val > (1.0 + eps)
|
|
235
239
|
|
|
236
|
-
if
|
|
237
|
-
cn.
|
|
240
|
+
if (
|
|
241
|
+
_out_of_bounds(cn.participation_weight)
|
|
242
|
+
or _out_of_bounds(cn.projected_participation_weight)
|
|
243
|
+
or _out_of_bounds(cn.defense_participation_weight)
|
|
244
|
+
or _out_of_bounds(cn.projected_defense_participation_weight)
|
|
238
245
|
):
|
|
239
246
|
self.scale_participation_weights = True
|
|
240
247
|
logging.warning(
|
|
@@ -289,6 +296,25 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
289
296
|
elif self._participation_weight_max is not None:
|
|
290
297
|
self._projected_participation_weight_max = self._participation_weight_max
|
|
291
298
|
|
|
299
|
+
if cn.defense_participation_weight and cn.defense_participation_weight in df.columns:
|
|
300
|
+
q_val = pl_df[cn.defense_participation_weight].quantile(0.99, "linear")
|
|
301
|
+
if q_val is not None:
|
|
302
|
+
self._defense_participation_weight_max = float(q_val)
|
|
303
|
+
elif self._participation_weight_max is not None:
|
|
304
|
+
self._defense_participation_weight_max = self._participation_weight_max
|
|
305
|
+
|
|
306
|
+
if (
|
|
307
|
+
cn.projected_defense_participation_weight
|
|
308
|
+
and cn.projected_defense_participation_weight in df.columns
|
|
309
|
+
):
|
|
310
|
+
q_val = pl_df[cn.projected_defense_participation_weight].quantile(0.99, "linear")
|
|
311
|
+
if q_val is not None:
|
|
312
|
+
self._projected_defense_participation_weight_max = float(q_val)
|
|
313
|
+
elif self._defense_participation_weight_max is not None:
|
|
314
|
+
self._projected_defense_participation_weight_max = self._defense_participation_weight_max
|
|
315
|
+
elif self._projected_participation_weight_max is not None:
|
|
316
|
+
self._projected_defense_participation_weight_max = self._projected_participation_weight_max
|
|
317
|
+
|
|
292
318
|
def _scale_participation_weight_columns(self, df: pl.DataFrame) -> pl.DataFrame:
|
|
293
319
|
"""Create internal scaled participation weight columns without mutating originals."""
|
|
294
320
|
if not self.scale_participation_weights:
|
|
@@ -321,6 +347,32 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
321
347
|
.alias(_SCALED_PPW)
|
|
322
348
|
)
|
|
323
349
|
|
|
350
|
+
if (
|
|
351
|
+
cn.defense_participation_weight
|
|
352
|
+
and cn.defense_participation_weight in df.columns
|
|
353
|
+
and self._defense_participation_weight_max is not None
|
|
354
|
+
and self._defense_participation_weight_max > 0
|
|
355
|
+
):
|
|
356
|
+
denom = float(self._defense_participation_weight_max)
|
|
357
|
+
df = df.with_columns(
|
|
358
|
+
(pl.col(cn.defense_participation_weight) / denom)
|
|
359
|
+
.clip(0.0, 1.0)
|
|
360
|
+
.alias(_SCALED_DPW)
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
if (
|
|
364
|
+
cn.projected_defense_participation_weight
|
|
365
|
+
and cn.projected_defense_participation_weight in df.columns
|
|
366
|
+
and self._projected_defense_participation_weight_max is not None
|
|
367
|
+
and self._projected_defense_participation_weight_max > 0
|
|
368
|
+
):
|
|
369
|
+
denom = float(self._projected_defense_participation_weight_max)
|
|
370
|
+
df = df.with_columns(
|
|
371
|
+
(pl.col(cn.projected_defense_participation_weight) / denom)
|
|
372
|
+
.clip(0.0, 1.0)
|
|
373
|
+
.alias(_SCALED_PDPW)
|
|
374
|
+
)
|
|
375
|
+
|
|
324
376
|
return df
|
|
325
377
|
|
|
326
378
|
def _get_participation_weight_col(self) -> str:
|
|
@@ -339,7 +391,9 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
339
391
|
|
|
340
392
|
def _remove_internal_scaled_columns(self, df: pl.DataFrame) -> pl.DataFrame:
|
|
341
393
|
"""Remove internal scaled columns before returning."""
|
|
342
|
-
cols_to_drop = [
|
|
394
|
+
cols_to_drop = [
|
|
395
|
+
c for c in [_SCALED_PW, _SCALED_PPW, _SCALED_DPW, _SCALED_PDPW] if c in df.columns
|
|
396
|
+
]
|
|
343
397
|
if cols_to_drop:
|
|
344
398
|
df = df.drop(cols_to_drop)
|
|
345
399
|
return df
|
|
@@ -554,7 +608,7 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
554
608
|
def_change = (
|
|
555
609
|
(def_perf - float(pred_def))
|
|
556
610
|
* mult_def
|
|
557
|
-
* float(pre_player.match_performance.
|
|
611
|
+
* float(pre_player.match_performance.defense_participation_weight)
|
|
558
612
|
)
|
|
559
613
|
|
|
560
614
|
if math.isnan(off_change) or math.isnan(def_change):
|
|
@@ -648,7 +702,7 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
648
702
|
def_change = (
|
|
649
703
|
(def_perf - float(pred_def))
|
|
650
704
|
* mult_def
|
|
651
|
-
* float(pre_player.match_performance.
|
|
705
|
+
* float(pre_player.match_performance.defense_participation_weight)
|
|
652
706
|
)
|
|
653
707
|
|
|
654
708
|
if math.isnan(off_change) or math.isnan(def_change):
|
|
@@ -922,6 +976,19 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
922
976
|
if _SCALED_PPW in df.columns:
|
|
923
977
|
player_stat_cols.append(_SCALED_PPW)
|
|
924
978
|
|
|
979
|
+
if cn.defense_participation_weight and cn.defense_participation_weight in df.columns:
|
|
980
|
+
player_stat_cols.append(cn.defense_participation_weight)
|
|
981
|
+
if _SCALED_DPW in df.columns:
|
|
982
|
+
player_stat_cols.append(_SCALED_DPW)
|
|
983
|
+
|
|
984
|
+
if (
|
|
985
|
+
cn.projected_defense_participation_weight
|
|
986
|
+
and cn.projected_defense_participation_weight in df.columns
|
|
987
|
+
):
|
|
988
|
+
player_stat_cols.append(cn.projected_defense_participation_weight)
|
|
989
|
+
if _SCALED_PDPW in df.columns:
|
|
990
|
+
player_stat_cols.append(_SCALED_PDPW)
|
|
991
|
+
|
|
925
992
|
if cn.position and cn.position in df.columns:
|
|
926
993
|
player_stat_cols.append(cn.position)
|
|
927
994
|
|
|
@@ -1041,6 +1108,28 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
1041
1108
|
projected_participation_weight = participation_weight
|
|
1042
1109
|
projected_participation_weights.append(projected_participation_weight)
|
|
1043
1110
|
|
|
1111
|
+
# Use scaled defense participation weight if available, otherwise default to participation_weight
|
|
1112
|
+
if _SCALED_DPW in team_player:
|
|
1113
|
+
defense_participation_weight = team_player.get(_SCALED_DPW, participation_weight)
|
|
1114
|
+
elif cn.defense_participation_weight:
|
|
1115
|
+
defense_participation_weight = team_player.get(
|
|
1116
|
+
cn.defense_participation_weight, participation_weight
|
|
1117
|
+
)
|
|
1118
|
+
else:
|
|
1119
|
+
defense_participation_weight = participation_weight
|
|
1120
|
+
|
|
1121
|
+
# Use scaled projected defense participation weight if available
|
|
1122
|
+
if _SCALED_PDPW in team_player:
|
|
1123
|
+
projected_defense_participation_weight = team_player.get(
|
|
1124
|
+
_SCALED_PDPW, defense_participation_weight
|
|
1125
|
+
)
|
|
1126
|
+
elif cn.projected_defense_participation_weight:
|
|
1127
|
+
projected_defense_participation_weight = team_player.get(
|
|
1128
|
+
cn.projected_defense_participation_weight, defense_participation_weight
|
|
1129
|
+
)
|
|
1130
|
+
else:
|
|
1131
|
+
projected_defense_participation_weight = defense_participation_weight
|
|
1132
|
+
|
|
1044
1133
|
perf_val = (
|
|
1045
1134
|
float(team_player[self.performance_column])
|
|
1046
1135
|
if (
|
|
@@ -1061,6 +1150,8 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
1061
1150
|
performance_value=perf_val,
|
|
1062
1151
|
projected_participation_weight=projected_participation_weight,
|
|
1063
1152
|
participation_weight=participation_weight,
|
|
1153
|
+
defense_participation_weight=defense_participation_weight,
|
|
1154
|
+
projected_defense_participation_weight=projected_defense_participation_weight,
|
|
1064
1155
|
team_players_playing_time=team_playing_time,
|
|
1065
1156
|
opponent_players_playing_time=opponent_playing_time,
|
|
1066
1157
|
)
|
|
@@ -1296,6 +1387,22 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
1296
1387
|
ppw = pw
|
|
1297
1388
|
proj_w.append(float(ppw))
|
|
1298
1389
|
|
|
1390
|
+
# Use scaled defense participation weight if available
|
|
1391
|
+
if _SCALED_DPW in tp:
|
|
1392
|
+
dpw = tp.get(_SCALED_DPW, pw)
|
|
1393
|
+
elif cn.defense_participation_weight:
|
|
1394
|
+
dpw = tp.get(cn.defense_participation_weight, pw)
|
|
1395
|
+
else:
|
|
1396
|
+
dpw = pw
|
|
1397
|
+
|
|
1398
|
+
# Use scaled projected defense participation weight if available
|
|
1399
|
+
if _SCALED_PDPW in tp:
|
|
1400
|
+
pdpw = tp.get(_SCALED_PDPW, dpw)
|
|
1401
|
+
elif cn.projected_defense_participation_weight:
|
|
1402
|
+
pdpw = tp.get(cn.projected_defense_participation_weight, dpw)
|
|
1403
|
+
else:
|
|
1404
|
+
pdpw = dpw
|
|
1405
|
+
|
|
1299
1406
|
team_playing_time = self._get_players_playing_time(
|
|
1300
1407
|
tp, cn.team_players_playing_time
|
|
1301
1408
|
)
|
|
@@ -1307,6 +1414,8 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
1307
1414
|
performance_value=get_perf_value(tp),
|
|
1308
1415
|
projected_participation_weight=ppw,
|
|
1309
1416
|
participation_weight=pw,
|
|
1417
|
+
defense_participation_weight=dpw,
|
|
1418
|
+
projected_defense_participation_weight=pdpw,
|
|
1310
1419
|
team_players_playing_time=team_playing_time,
|
|
1311
1420
|
opponent_players_playing_time=opponent_playing_time,
|
|
1312
1421
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spforge
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.30
|
|
4
4
|
Summary: A flexible framework for generating features, ratings, and building machine learning or other models for training and inference on sports data.
|
|
5
5
|
Author-email: Mathias Holmstrøm <mathiasholmstom@gmail.com>
|
|
6
6
|
License: See LICENSE file
|
|
@@ -16,7 +16,7 @@ examples/nba/data/utils.py,sha256=41hxLQ1d6ZgBEcHa5MI0-fG5KbsRi07cclMPQZM95ek,50
|
|
|
16
16
|
spforge/__init__.py,sha256=8vZhy7XUpzqWkVKpXqwqOLDkQlNytRhyf4qjwObfXgU,468
|
|
17
17
|
spforge/autopipeline.py,sha256=rZ6FhJxcgNLvtr3hTVkEiW4BiorgXxADThfMuQ42orE,29866
|
|
18
18
|
spforge/base_feature_generator.py,sha256=RbD00N6oLCQQcEb_VF5wbwZztl-X8k9B0Wlaj9Os1iU,668
|
|
19
|
-
spforge/data_structures.py,sha256=
|
|
19
|
+
spforge/data_structures.py,sha256=AltcyPvEI2qLuk43qwnljTj-QZzLMw1UEL6-lWQvqLQ,7530
|
|
20
20
|
spforge/features_generator_pipeline.py,sha256=n8vzZKqXNFcFRDWZhllnkhAh5NFXdOD3FEIOpHcay8E,8208
|
|
21
21
|
spforge/utils.py,sha256=2RlivUtMX5wQWpFVUyFfexDJE0wV6uZ4dnNzvoDmVhI,2644
|
|
22
22
|
spforge/cross_validator/__init__.py,sha256=1QHgTFIZ73EZ_MgJlUKimxdUmB7MFaOEy6jsUs6V0T0,134
|
|
@@ -51,7 +51,7 @@ spforge/performance_transformers/_performance_manager.py,sha256=WmjmlMEnq7y75MiI
|
|
|
51
51
|
spforge/performance_transformers/_performances_transformers.py,sha256=0lxuWjAfWBRXRgQsNJHjw3P-nlTtHBu4_bOVdoy7hq4,15536
|
|
52
52
|
spforge/ratings/__init__.py,sha256=OZVH2Lo6END3n1X8qi4QcyAPlThIwAYwVKCiIuOQSQU,576
|
|
53
53
|
spforge/ratings/_base.py,sha256=ne4BRrYFPqMirdFPVnyDN44wjFQwOQgWoUXu_59xgWE,14687
|
|
54
|
-
spforge/ratings/_player_rating.py,sha256=
|
|
54
|
+
spforge/ratings/_player_rating.py,sha256=0VZYTWdoZoxPpw1UhTsRxlwMJjBAGNr2EdGOQkT2BpE,67097
|
|
55
55
|
spforge/ratings/_team_rating.py,sha256=3m90-R2zW0k5EHwjw-83Hacz91fGmxW1LQ8ZUGHlgt4,24970
|
|
56
56
|
spforge/ratings/enums.py,sha256=s7z_RcZS6Nlgfa_6tasO8_IABZJwywexe7sep9DJBgo,1739
|
|
57
57
|
spforge/ratings/league_identifier.py,sha256=_KDUKOwoNU6RNFKE5jju4eYFGVNGBdJsv5mhNvMakfc,6019
|
|
@@ -71,7 +71,7 @@ spforge/transformers/_other_transformer.py,sha256=w2a7Wnki3vJe4GAkSa4kealw0GILIo
|
|
|
71
71
|
spforge/transformers/_predictor.py,sha256=2sE6gfVrilXzPVcBurSrtqHw33v2ljygQcEYXt9LhZc,3119
|
|
72
72
|
spforge/transformers/_simple_transformer.py,sha256=zGUFNQYMeoDSa2CoQejQNiNmKCBN5amWTvyOchiUHj0,5660
|
|
73
73
|
spforge/transformers/_team_ratio_predictor.py,sha256=g8_bR53Yyv0iNCtol1O9bgJSeZcIco_AfbQuUxQJkeY,6884
|
|
74
|
-
spforge-0.8.
|
|
74
|
+
spforge-0.8.30.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
75
75
|
tests/test_autopipeline.py,sha256=7cNAn-nmGolfyfk3THh9IKcHZfRA-pLYC_xAyMg-No4,26863
|
|
76
76
|
tests/test_autopipeline_context.py,sha256=IuRUY4IA6uMObvbl2pXSaXO2_tl3qX6wEbTZY0dkTMI,1240
|
|
77
77
|
tests/test_feature_generator_pipeline.py,sha256=CK0zVL8PfTncy3RmG9i-YpgwjOIV7yJhV7Q44tbetI8,19020
|
|
@@ -94,7 +94,7 @@ tests/hyperparameter_tuning/test_estimator_tuner.py,sha256=iewME41d6LR2aQ0OtohGF
|
|
|
94
94
|
tests/hyperparameter_tuning/test_rating_tuner.py,sha256=usjC2ioO_yWRjjNAlRTyMVYheOrCi0kKocmHQHdTmpM,18699
|
|
95
95
|
tests/performance_transformers/test_performance_manager.py,sha256=gjuuV_hb27kCo_kUecPKG3Cbot2Gqis1W3kw2A4ovS4,10690
|
|
96
96
|
tests/performance_transformers/test_performances_transformers.py,sha256=A-tGiCx7kXrj1cVj03Bc7prOeZ1_Ryz8YFx9uj3eK6w,11064
|
|
97
|
-
tests/ratings/test_player_rating_generator.py,sha256=
|
|
97
|
+
tests/ratings/test_player_rating_generator.py,sha256=F4mW7J4djkFpt0GgORIfVz0jKegfGNwPqGtXp44VOSc,100762
|
|
98
98
|
tests/ratings/test_player_rating_no_mutation.py,sha256=GzO3Hl__5K68DS3uRLefwnbcTJOvBM7cZqww4M21UZM,8493
|
|
99
99
|
tests/ratings/test_ratings_property.py,sha256=ckyfGILXa4tfQvsgyXEzBDNr2DUmHwFRV13N60w66iE,6561
|
|
100
100
|
tests/ratings/test_team_rating_generator.py,sha256=SqQcfckNmJJc99feCdnmkNYDape-p69e92Dp8Vzpu2w,101156
|
|
@@ -108,7 +108,7 @@ tests/transformers/test_other_transformer.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRk
|
|
|
108
108
|
tests/transformers/test_predictor_transformer.py,sha256=N1aBYLjN3ldpYZLwjih_gTFYSMitrZu-PNK78W6RHaQ,6877
|
|
109
109
|
tests/transformers/test_simple_transformer.py,sha256=wWR0qjLb_uS4HXrJgGdiqugOY1X7kwd1_OPS02IT2b8,4676
|
|
110
110
|
tests/transformers/test_team_ratio_predictor.py,sha256=fOUP_JvNJi-3kom3ZOs1EdG0I6Z8hpLpYKNHu1eWtOw,8562
|
|
111
|
-
spforge-0.8.
|
|
112
|
-
spforge-0.8.
|
|
113
|
-
spforge-0.8.
|
|
114
|
-
spforge-0.8.
|
|
111
|
+
spforge-0.8.30.dist-info/METADATA,sha256=DHqd51r8ONs36cHM0-CaWQJW_4QIKmX5MDNvl-2xTfo,20048
|
|
112
|
+
spforge-0.8.30.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
113
|
+
spforge-0.8.30.dist-info/top_level.txt,sha256=6UW2M5a7WKOeaAi900qQmRKNj5-HZzE8-eUD9Y9LTq0,23
|
|
114
|
+
spforge-0.8.30.dist-info/RECORD,,
|
|
@@ -2710,3 +2710,114 @@ def test_ignore_opponent_predictor_reference_rating_set_correctly(base_cn):
|
|
|
2710
2710
|
assert gen5._performance_predictor._reference_rating == 1200.0, (
|
|
2711
2711
|
f"Expected hardcoded start rating 1200.0 to take precedence, got {gen5._performance_predictor._reference_rating}"
|
|
2712
2712
|
)
|
|
2713
|
+
|
|
2714
|
+
|
|
2715
|
+
def test_separate_offense_defense_participation_weights(base_cn):
|
|
2716
|
+
"""Test that offense and defense use separate participation weights.
|
|
2717
|
+
|
|
2718
|
+
When participation_weight represents offensive activity (e.g., shots attempted),
|
|
2719
|
+
using it for both offense and defense updates creates bias. This test verifies
|
|
2720
|
+
that defense_participation_weight is used for defensive rating updates.
|
|
2721
|
+
"""
|
|
2722
|
+
from dataclasses import replace
|
|
2723
|
+
|
|
2724
|
+
cn = replace(
|
|
2725
|
+
base_cn,
|
|
2726
|
+
participation_weight="shots_attempted",
|
|
2727
|
+
defense_participation_weight="minutes",
|
|
2728
|
+
)
|
|
2729
|
+
|
|
2730
|
+
# Create a scenario where a high-volume shooter (many shots) faces a low-volume shooter
|
|
2731
|
+
# The high-volume shooter should have larger offensive updates but equal defensive updates
|
|
2732
|
+
df = pl.DataFrame(
|
|
2733
|
+
{
|
|
2734
|
+
"pid": ["P1", "P2", "P3", "P4"],
|
|
2735
|
+
"tid": ["T1", "T1", "T2", "T2"],
|
|
2736
|
+
"mid": ["M1", "M1", "M1", "M1"],
|
|
2737
|
+
"dt": ["2024-01-01"] * 4,
|
|
2738
|
+
"perf": [0.6, 0.4, 0.5, 0.5], # Varying performance values
|
|
2739
|
+
"shots_attempted": [10.0, 10.0, 10.0, 10.0], # Same offensive activity
|
|
2740
|
+
"minutes": [30.0, 30.0, 30.0, 30.0], # Same defensive activity
|
|
2741
|
+
}
|
|
2742
|
+
)
|
|
2743
|
+
|
|
2744
|
+
gen = PlayerRatingGenerator(
|
|
2745
|
+
performance_column="perf",
|
|
2746
|
+
column_names=cn,
|
|
2747
|
+
auto_scale_performance=True,
|
|
2748
|
+
rating_change_multiplier_offense=50,
|
|
2749
|
+
rating_change_multiplier_defense=50,
|
|
2750
|
+
)
|
|
2751
|
+
|
|
2752
|
+
result = gen.fit_transform(df)
|
|
2753
|
+
|
|
2754
|
+
# Verify that the defense_participation_weight column is present in the data
|
|
2755
|
+
assert "minutes" in df.columns
|
|
2756
|
+
|
|
2757
|
+
# All players performed equally (0.5) with equal participation weights,
|
|
2758
|
+
# so ratings should be symmetric
|
|
2759
|
+
assert "P1" in gen._player_off_ratings
|
|
2760
|
+
assert "P1" in gen._player_def_ratings
|
|
2761
|
+
|
|
2762
|
+
# Now test with different participation weights for offense vs defense
|
|
2763
|
+
df2 = pl.DataFrame(
|
|
2764
|
+
{
|
|
2765
|
+
"pid": ["P1", "P2", "P3", "P4"],
|
|
2766
|
+
"tid": ["T1", "T1", "T2", "T2"],
|
|
2767
|
+
"mid": ["M2", "M2", "M2", "M2"],
|
|
2768
|
+
"dt": ["2024-01-02"] * 4,
|
|
2769
|
+
"perf": [0.6, 0.4, 0.5, 0.5],
|
|
2770
|
+
"shots_attempted": [20.0, 5.0, 10.0, 10.0], # P1 shoots much more
|
|
2771
|
+
"minutes": [30.0, 30.0, 30.0, 30.0], # But all play same minutes
|
|
2772
|
+
}
|
|
2773
|
+
)
|
|
2774
|
+
|
|
2775
|
+
result2 = gen.fit_transform(df2)
|
|
2776
|
+
|
|
2777
|
+
# P1 should have larger offensive rating changes due to high shots_attempted
|
|
2778
|
+
# but equal defensive rating changes due to equal minutes played
|
|
2779
|
+
p1_off = gen._player_off_ratings["P1"]
|
|
2780
|
+
p2_off = gen._player_off_ratings["P2"]
|
|
2781
|
+
p1_def = gen._player_def_ratings["P1"]
|
|
2782
|
+
p2_def = gen._player_def_ratings["P2"]
|
|
2783
|
+
|
|
2784
|
+
# Both players have same games_played count for defense
|
|
2785
|
+
assert p1_def.games_played == p2_def.games_played
|
|
2786
|
+
|
|
2787
|
+
# Verify that ratings were updated
|
|
2788
|
+
assert p1_off.games_played > 0
|
|
2789
|
+
assert p2_off.games_played > 0
|
|
2790
|
+
|
|
2791
|
+
|
|
2792
|
+
@pytest.mark.parametrize("library", ["polars", "pandas"])
|
|
2793
|
+
def test_defense_participation_weight_backwards_compatibility(base_cn, library):
|
|
2794
|
+
"""Test that when defense_participation_weight is not set, it defaults to participation_weight."""
|
|
2795
|
+
import pandas as pd
|
|
2796
|
+
|
|
2797
|
+
df_data = {
|
|
2798
|
+
"pid": ["P1", "P2", "P3", "P4"],
|
|
2799
|
+
"tid": ["T1", "T1", "T2", "T2"],
|
|
2800
|
+
"mid": ["M1", "M1", "M1", "M1"],
|
|
2801
|
+
"dt": ["2024-01-01"] * 4,
|
|
2802
|
+
"perf": [0.6, 0.4, 0.5, 0.5],
|
|
2803
|
+
"pw": [1.0, 0.5, 0.8, 0.8],
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
if library == "polars":
|
|
2807
|
+
df = pl.DataFrame(df_data)
|
|
2808
|
+
else:
|
|
2809
|
+
df = pd.DataFrame(df_data)
|
|
2810
|
+
|
|
2811
|
+
# When defense_participation_weight is None, it should default to participation_weight
|
|
2812
|
+
gen = PlayerRatingGenerator(
|
|
2813
|
+
performance_column="perf",
|
|
2814
|
+
column_names=base_cn,
|
|
2815
|
+
auto_scale_performance=True,
|
|
2816
|
+
)
|
|
2817
|
+
|
|
2818
|
+
result = gen.fit_transform(df)
|
|
2819
|
+
|
|
2820
|
+
# Should work without errors
|
|
2821
|
+
assert result is not None
|
|
2822
|
+
assert len(gen._player_off_ratings) > 0
|
|
2823
|
+
assert len(gen._player_def_ratings) > 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|