spforge 0.8.20__tar.gz → 0.8.23__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.
Potentially problematic release.
This version of spforge might be problematic. Click here for more details.
- {spforge-0.8.20/spforge.egg-info → spforge-0.8.23}/PKG-INFO +1 -1
- {spforge-0.8.20 → spforge-0.8.23}/pyproject.toml +1 -1
- {spforge-0.8.20 → spforge-0.8.23}/spforge/ratings/_base.py +6 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/ratings/_player_rating.py +40 -27
- {spforge-0.8.20 → spforge-0.8.23}/spforge/ratings/_team_rating.py +23 -20
- {spforge-0.8.20 → spforge-0.8.23/spforge.egg-info}/PKG-INFO +1 -1
- {spforge-0.8.20 → spforge-0.8.23}/tests/ratings/test_player_rating_generator.py +149 -118
- {spforge-0.8.20 → spforge-0.8.23}/tests/ratings/test_team_rating_generator.py +153 -11
- {spforge-0.8.20 → spforge-0.8.23}/LICENSE +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/MANIFEST.in +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/README.md +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/examples/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/examples/game_level_example.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/examples/lol/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/examples/lol/data/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/examples/lol/data/subsample_lol_data.parquet +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/examples/lol/data/utils.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/examples/lol/pipeline_transformer_example.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/examples/nba/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/examples/nba/cross_validation_example.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/examples/nba/data/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/examples/nba/data/game_player_subsample.parquet +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/examples/nba/data/utils.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/examples/nba/feature_engineering_example.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/examples/nba/game_winner_example.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/examples/nba/predictor_transformers_example.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/setup.cfg +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/autopipeline.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/base_feature_generator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/cross_validator/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/cross_validator/_base.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/cross_validator/cross_validator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/data_structures.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/distributions/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/distributions/_negative_binomial_estimator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/distributions/_normal_distribution_predictor.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/distributions/_student_t_distribution_estimator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/estimator/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/estimator/_conditional_estimator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/estimator/_frequency_bucketing_classifier.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/estimator/_granularity_estimator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/estimator/_group_by_estimator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/estimator/_ordinal_classifier.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/estimator/_sklearn_enhancer_estimator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/feature_generator/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/feature_generator/_base.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/feature_generator/_lag.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/feature_generator/_net_over_predicted.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/feature_generator/_regressor_feature_generator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/feature_generator/_rolling_against_opponent.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/feature_generator/_rolling_mean_binary.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/feature_generator/_rolling_mean_days.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/feature_generator/_rolling_window.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/feature_generator/_utils.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/features_generator_pipeline.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/hyperparameter_tuning/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/hyperparameter_tuning/_default_search_spaces.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/hyperparameter_tuning/_tuner.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/performance_transformers/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/performance_transformers/_performance_manager.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/performance_transformers/_performances_transformers.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/ratings/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/ratings/enums.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/ratings/league_identifier.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/ratings/league_start_rating_optimizer.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/ratings/player_performance_predictor.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/ratings/start_rating_generator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/ratings/team_performance_predictor.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/ratings/team_start_rating_generator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/ratings/utils.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/scorer/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/scorer/_score.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/transformers/__init__.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/transformers/_base.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/transformers/_net_over_predicted.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/transformers/_operator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/transformers/_other_transformer.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/transformers/_predictor.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/transformers/_simple_transformer.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/transformers/_team_ratio_predictor.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge/utils.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge.egg-info/SOURCES.txt +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge.egg-info/dependency_links.txt +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge.egg-info/requires.txt +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/spforge.egg-info/top_level.txt +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/cross_validator/test_cross_validator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/distributions/test_distribution.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/end_to_end/test_estimator_hyperparameter_tuning.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/end_to_end/test_league_start_rating_optimizer.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/end_to_end/test_lol_player_kills.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/end_to_end/test_nba_player_points.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/end_to_end/test_nba_player_ratings_hyperparameter_tuning.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/end_to_end/test_nba_prediction_consistency.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/estimator/test_sklearn_estimator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/feature_generator/test_lag.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/feature_generator/test_regressor_feature_generator.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/feature_generator/test_rolling_against_opponent.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/feature_generator/test_rolling_mean_binary.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/feature_generator/test_rolling_mean_days.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/feature_generator/test_rolling_window.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/hyperparameter_tuning/test_estimator_tuner.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/hyperparameter_tuning/test_rating_tuner.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/performance_transformers/test_performance_manager.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/performance_transformers/test_performances_transformers.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/ratings/test_player_rating_no_mutation.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/ratings/test_ratings_property.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/ratings/test_utils_scaled_weights.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/scorer/test_score.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/scorer/test_score_aggregation_granularity.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/test_autopipeline.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/test_autopipeline_context.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/test_feature_generator_pipeline.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/transformers/test_estimator_transformer_context.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/transformers/test_net_over_predicted.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/transformers/test_other_transformer.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/transformers/test_predictor_transformer.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/transformers/test_simple_transformer.py +0 -0
- {spforge-0.8.20 → spforge-0.8.23}/tests/transformers/test_team_ratio_predictor.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spforge
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.23
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "spforge"
|
|
7
|
-
version = "0.8.
|
|
7
|
+
version = "0.8.23"
|
|
8
8
|
description = "A flexible framework for generating features, ratings, and building machine learning or other models for training and inference on sports data."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -176,6 +176,9 @@ class RatingGenerator(FeatureGenerator):
|
|
|
176
176
|
@to_polars
|
|
177
177
|
@nw.narwhalify
|
|
178
178
|
def transform(self, df: IntoFrameT) -> IntoFrameT:
|
|
179
|
+
if self.performance_manager and self.performance_manager.ori_performance_column in df.columns:
|
|
180
|
+
df = nw.from_native(self.performance_manager.transform(df))
|
|
181
|
+
|
|
179
182
|
pl_df: pl.DataFrame
|
|
180
183
|
pl_df = df.to_native() if df.implementation.is_polars() else df.to_polars().to_native()
|
|
181
184
|
return self._historical_transform(pl_df)
|
|
@@ -188,6 +191,9 @@ class RatingGenerator(FeatureGenerator):
|
|
|
188
191
|
- use existing ratings to compute pre-match ratings/features
|
|
189
192
|
- do NOT update ratings
|
|
190
193
|
"""
|
|
194
|
+
if self.performance_manager and self.performance_manager.ori_performance_column in df.columns:
|
|
195
|
+
df = nw.from_native(self.performance_manager.transform(df))
|
|
196
|
+
|
|
191
197
|
pl_df: pl.DataFrame
|
|
192
198
|
pl_df = df.to_native() if df.implementation.is_polars() else df.to_polars().to_native()
|
|
193
199
|
return self._future_transform(pl_df)
|
|
@@ -433,9 +433,16 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
433
433
|
team1_off_perf = self._team_off_perf_from_collection(c1)
|
|
434
434
|
team2_off_perf = self._team_off_perf_from_collection(c2)
|
|
435
435
|
|
|
436
|
+
team1_def_perf: float | None = None
|
|
437
|
+
team2_def_perf: float | None = None
|
|
438
|
+
|
|
436
439
|
if self.use_off_def_split:
|
|
437
|
-
team1_def_perf =
|
|
438
|
-
|
|
440
|
+
team1_def_perf = (
|
|
441
|
+
1.0 - team2_off_perf if team2_off_perf is not None else None
|
|
442
|
+
)
|
|
443
|
+
team2_def_perf = (
|
|
444
|
+
1.0 - team1_off_perf if team1_off_perf is not None else None
|
|
445
|
+
)
|
|
439
446
|
else:
|
|
440
447
|
team1_def_perf = team1_off_perf
|
|
441
448
|
team2_def_perf = team2_off_perf
|
|
@@ -487,23 +494,25 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
487
494
|
perf_value = pre_player.match_performance.performance_value
|
|
488
495
|
if perf_value is None:
|
|
489
496
|
off_change = 0.0
|
|
490
|
-
def_change = 0.0
|
|
491
497
|
else:
|
|
492
498
|
off_perf = float(perf_value)
|
|
493
|
-
def_perf = float(team1_def_perf)
|
|
494
|
-
|
|
495
|
-
if not self.use_off_def_split:
|
|
496
|
-
pred_def = pred_off
|
|
497
|
-
def_perf = off_perf
|
|
498
|
-
|
|
499
499
|
mult_off = self._applied_multiplier_off(off_state)
|
|
500
|
-
mult_def = self._applied_multiplier_def(def_state)
|
|
501
|
-
|
|
502
500
|
off_change = (
|
|
503
501
|
(off_perf - float(pred_off))
|
|
504
502
|
* mult_off
|
|
505
503
|
* float(pre_player.match_performance.participation_weight)
|
|
506
504
|
)
|
|
505
|
+
|
|
506
|
+
if perf_value is None or team1_def_perf is None:
|
|
507
|
+
def_change = 0.0
|
|
508
|
+
else:
|
|
509
|
+
def_perf = float(team1_def_perf)
|
|
510
|
+
|
|
511
|
+
if not self.use_off_def_split:
|
|
512
|
+
pred_def = pred_off
|
|
513
|
+
def_perf = float(perf_value)
|
|
514
|
+
|
|
515
|
+
mult_def = self._applied_multiplier_def(def_state)
|
|
507
516
|
def_change = (
|
|
508
517
|
(def_perf - float(pred_def))
|
|
509
518
|
* mult_def
|
|
@@ -570,33 +579,35 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
570
579
|
perf_value = pre_player.match_performance.performance_value
|
|
571
580
|
if perf_value is None:
|
|
572
581
|
off_change = 0.0
|
|
573
|
-
def_change = 0.0
|
|
574
582
|
else:
|
|
575
583
|
off_perf = float(perf_value)
|
|
576
|
-
def_perf = float(team2_def_perf)
|
|
577
|
-
|
|
578
|
-
if not self.use_off_def_split:
|
|
579
|
-
pred_def = pred_off
|
|
580
|
-
def_perf = off_perf
|
|
581
|
-
|
|
582
584
|
mult_off = self._applied_multiplier_off(off_state)
|
|
583
|
-
mult_def = self._applied_multiplier_def(def_state)
|
|
584
|
-
|
|
585
585
|
off_change = (
|
|
586
586
|
(off_perf - float(pred_off))
|
|
587
587
|
* mult_off
|
|
588
588
|
* float(pre_player.match_performance.participation_weight)
|
|
589
589
|
)
|
|
590
|
+
|
|
591
|
+
if perf_value is None or team2_def_perf is None:
|
|
592
|
+
def_change = 0.0
|
|
593
|
+
else:
|
|
594
|
+
def_perf = float(team2_def_perf)
|
|
595
|
+
|
|
596
|
+
if not self.use_off_def_split:
|
|
597
|
+
pred_def = pred_off
|
|
598
|
+
def_perf = float(perf_value)
|
|
599
|
+
|
|
600
|
+
mult_def = self._applied_multiplier_def(def_state)
|
|
590
601
|
def_change = (
|
|
591
602
|
(def_perf - float(pred_def))
|
|
592
603
|
* mult_def
|
|
593
604
|
* float(pre_player.match_performance.participation_weight)
|
|
594
605
|
)
|
|
595
606
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
607
|
+
if math.isnan(off_change) or math.isnan(def_change):
|
|
608
|
+
raise ValueError(
|
|
609
|
+
f"NaN player rating change for player_id={pid}, match_id={r[cn.match_id]}"
|
|
610
|
+
)
|
|
600
611
|
|
|
601
612
|
player_updates.append(
|
|
602
613
|
(
|
|
@@ -1031,12 +1042,14 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
1031
1042
|
|
|
1032
1043
|
return pre_match_player_ratings, pre_match_player_off_values
|
|
1033
1044
|
|
|
1034
|
-
def _team_off_perf_from_collection(
|
|
1045
|
+
def _team_off_perf_from_collection(
|
|
1046
|
+
self, c: PreMatchPlayersCollection
|
|
1047
|
+
) -> float | None:
|
|
1035
1048
|
# observed offense perf = weighted mean of player performance_value using participation_weight if present
|
|
1036
1049
|
# skip players with null performance
|
|
1037
1050
|
cn = self.column_names
|
|
1038
1051
|
if not c.pre_match_player_ratings:
|
|
1039
|
-
return
|
|
1052
|
+
return None
|
|
1040
1053
|
wsum = 0.0
|
|
1041
1054
|
psum = 0.0
|
|
1042
1055
|
for pre in c.pre_match_player_ratings:
|
|
@@ -1050,7 +1063,7 @@ class PlayerRatingGenerator(RatingGenerator):
|
|
|
1050
1063
|
)
|
|
1051
1064
|
psum += float(perf_val) * w
|
|
1052
1065
|
wsum += w
|
|
1053
|
-
return psum / wsum if wsum else
|
|
1066
|
+
return psum / wsum if wsum else None
|
|
1054
1067
|
|
|
1055
1068
|
def _team_off_def_rating_from_collection(
|
|
1056
1069
|
self, c: PreMatchPlayersCollection
|
|
@@ -326,16 +326,7 @@ class TeamRatingGenerator(RatingGenerator):
|
|
|
326
326
|
opp_off_pre = float(o_off.rating_value)
|
|
327
327
|
opp_def_pre = float(o_def.rating_value)
|
|
328
328
|
|
|
329
|
-
|
|
330
|
-
float(r[self.performance_column])
|
|
331
|
-
if r.get(self.performance_column) is not None
|
|
332
|
-
else 0.0
|
|
333
|
-
)
|
|
334
|
-
opp_off_perf = float(r[perf_opp_col]) if r.get(perf_opp_col) is not None else 0.0
|
|
335
|
-
if self.use_off_def_split:
|
|
336
|
-
def_perf = 1.0 - opp_off_perf
|
|
337
|
-
else:
|
|
338
|
-
def_perf = off_perf
|
|
329
|
+
off_perf_raw = r.get(self.performance_column)
|
|
339
330
|
|
|
340
331
|
pred_off = self._performance_predictor.predict_performance(
|
|
341
332
|
rating_value=s_off.rating_value, opponent_team_rating_value=o_def.rating_value
|
|
@@ -346,16 +337,28 @@ class TeamRatingGenerator(RatingGenerator):
|
|
|
346
337
|
if not self.use_off_def_split:
|
|
347
338
|
pred_def = pred_off
|
|
348
339
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
340
|
+
# Null performance means no rating change
|
|
341
|
+
if off_perf_raw is None:
|
|
342
|
+
off_change = 0.0
|
|
343
|
+
def_change = 0.0
|
|
344
|
+
else:
|
|
345
|
+
off_perf = float(off_perf_raw)
|
|
346
|
+
opp_off_perf = float(r[perf_opp_col]) if r.get(perf_opp_col) is not None else 0.0
|
|
347
|
+
if self.use_off_def_split:
|
|
348
|
+
def_perf = 1.0 - opp_off_perf
|
|
349
|
+
else:
|
|
350
|
+
def_perf = off_perf
|
|
351
|
+
|
|
352
|
+
mult_off = self._applied_multiplier(s_off, self.rating_change_multiplier_offense)
|
|
353
|
+
mult_def = self._applied_multiplier(s_def, self.rating_change_multiplier_defense)
|
|
354
|
+
|
|
355
|
+
off_change = (off_perf - pred_off) * mult_off
|
|
356
|
+
def_change = (def_perf - pred_def) * mult_def
|
|
357
|
+
|
|
358
|
+
if math.isnan(off_change) or math.isnan(def_change):
|
|
359
|
+
raise ValueError(
|
|
360
|
+
f"NaN rating change for team_id={team_id}, match_id={r[cn.match_id]}"
|
|
361
|
+
)
|
|
359
362
|
|
|
360
363
|
rows.append(
|
|
361
364
|
{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spforge
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.23
|
|
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
|
|
@@ -107,6 +107,7 @@ def test_fit_transform_participation_weight_scaling(base_cn):
|
|
|
107
107
|
|
|
108
108
|
def test_fit_transform_batch_update_logic(base_cn):
|
|
109
109
|
"""Test that ratings do not update between matches if update_match_id is the same."""
|
|
110
|
+
from dataclasses import replace
|
|
110
111
|
|
|
111
112
|
df = pl.DataFrame(
|
|
112
113
|
{
|
|
@@ -119,36 +120,25 @@ def test_fit_transform_batch_update_logic(base_cn):
|
|
|
119
120
|
"pw": [1.0, 1.0, 1.0, 1.0],
|
|
120
121
|
}
|
|
121
122
|
)
|
|
122
|
-
from dataclasses import replace
|
|
123
123
|
|
|
124
124
|
cn = replace(base_cn, update_match_id="update_id")
|
|
125
125
|
gen = PlayerRatingGenerator(
|
|
126
|
-
performance_column="perf",
|
|
126
|
+
performance_column="perf",
|
|
127
|
+
column_names=cn,
|
|
128
|
+
auto_scale_performance=True,
|
|
129
|
+
features_out=[RatingKnownFeatures.PLAYER_OFF_RATING],
|
|
127
130
|
)
|
|
128
131
|
output = gen.fit_transform(df)
|
|
129
132
|
|
|
130
|
-
assert len(output)
|
|
131
|
-
|
|
132
|
-
assert len(gen._player_off_ratings) > 0
|
|
133
|
+
assert len(output) == 4
|
|
133
134
|
|
|
135
|
+
m1_rows = output.filter(pl.col("mid") == "M1")
|
|
136
|
+
m2_rows = output.filter(pl.col("mid") == "M2")
|
|
137
|
+
assert m1_rows["player_off_rating_perf"][0] == 1000.0
|
|
138
|
+
assert m2_rows["player_off_rating_perf"][0] == 1000.0
|
|
134
139
|
|
|
135
|
-
|
|
136
|
-
"
|
|
137
|
-
[
|
|
138
|
-
RatingKnownFeatures.PLAYER_OFF_RATING,
|
|
139
|
-
RatingKnownFeatures.TEAM_OFF_RATING_PROJECTED,
|
|
140
|
-
RatingKnownFeatures.TEAM_RATING_DIFFERENCE_PROJECTED,
|
|
141
|
-
],
|
|
142
|
-
)
|
|
143
|
-
def test_fit_transform_requested_features_presence(base_cn, sample_df, feature):
|
|
144
|
-
"""Verify that specific requested features appear in the resulting DataFrame."""
|
|
145
|
-
gen = PlayerRatingGenerator(
|
|
146
|
-
performance_column="perf", column_names=base_cn, features_out=[feature]
|
|
147
|
-
)
|
|
148
|
-
res = gen.fit_transform(sample_df)
|
|
149
|
-
|
|
150
|
-
expected_col = f"{feature}_perf"
|
|
151
|
-
assert expected_col in res.columns
|
|
140
|
+
assert gen._player_off_ratings["P1"].rating_value > 1000.0
|
|
141
|
+
assert gen._player_off_ratings["P2"].rating_value < 1000.0
|
|
152
142
|
|
|
153
143
|
|
|
154
144
|
def test_future_transform_no_state_mutation(base_cn, sample_df):
|
|
@@ -170,7 +160,10 @@ def test_future_transform_no_state_mutation(base_cn, sample_df):
|
|
|
170
160
|
def test_future_transform_cold_start_player(base_cn, sample_df):
|
|
171
161
|
"""Check that future_transform handles players not seen during fit_transform."""
|
|
172
162
|
gen = PlayerRatingGenerator(
|
|
173
|
-
performance_column="perf",
|
|
163
|
+
performance_column="perf",
|
|
164
|
+
column_names=base_cn,
|
|
165
|
+
auto_scale_performance=True,
|
|
166
|
+
features_out=[RatingKnownFeatures.PLAYER_OFF_RATING],
|
|
174
167
|
)
|
|
175
168
|
gen.fit_transform(sample_df)
|
|
176
169
|
|
|
@@ -187,12 +180,16 @@ def test_future_transform_cold_start_player(base_cn, sample_df):
|
|
|
187
180
|
res = gen.future_transform(new_player_df)
|
|
188
181
|
|
|
189
182
|
assert "P99" in res["pid"].to_list()
|
|
183
|
+
assert len(res) == 4
|
|
190
184
|
|
|
191
|
-
|
|
185
|
+
p99_row = res.filter(pl.col("pid") == "P99")
|
|
186
|
+
assert p99_row["player_off_rating_perf"][0] == 1000.0
|
|
192
187
|
|
|
193
188
|
|
|
194
189
|
def test_transform_is_identical_to_future_transform(base_cn, sample_df):
|
|
195
190
|
"""Verify that the standard transform() call redirects to future_transform logic."""
|
|
191
|
+
import polars.testing as pl_testing
|
|
192
|
+
|
|
196
193
|
gen = PlayerRatingGenerator(
|
|
197
194
|
performance_column="perf", column_names=base_cn, auto_scale_performance=True
|
|
198
195
|
)
|
|
@@ -202,9 +199,10 @@ def test_transform_is_identical_to_future_transform(base_cn, sample_df):
|
|
|
202
199
|
res_transform = gen.transform(sample_df)
|
|
203
200
|
res_future = gen.future_transform(sample_df)
|
|
204
201
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
202
|
+
pl_testing.assert_frame_equal(
|
|
203
|
+
res_transform.select(sorted(res_transform.columns)).sort("pid"),
|
|
204
|
+
res_future.select(sorted(res_future.columns)).sort("pid"),
|
|
205
|
+
)
|
|
208
206
|
|
|
209
207
|
|
|
210
208
|
def test_fit_transform_offense_defense_independence(base_cn):
|
|
@@ -406,9 +404,11 @@ def test_fit_transform_when_date_formats_vary_then_processes_successfully_player
|
|
|
406
404
|
|
|
407
405
|
result = gen.fit_transform(df)
|
|
408
406
|
|
|
409
|
-
assert len(result)
|
|
407
|
+
assert len(result) == 4
|
|
410
408
|
|
|
411
|
-
assert
|
|
409
|
+
assert gen._player_off_ratings["P1"].rating_value > 1000.0
|
|
410
|
+
assert gen._player_off_ratings["P2"].rating_value < 1000.0
|
|
411
|
+
assert gen._player_off_ratings["P3"].rating_value > gen._player_off_ratings["P4"].rating_value
|
|
412
412
|
|
|
413
413
|
|
|
414
414
|
@pytest.mark.parametrize(
|
|
@@ -710,17 +710,18 @@ def test_fit_transform_confidence_decay_over_time(base_cn):
|
|
|
710
710
|
|
|
711
711
|
|
|
712
712
|
def test_fit_transform_null_performance_handling(base_cn, sample_df):
|
|
713
|
-
"""Rows with null performance should be handled
|
|
713
|
+
"""Rows with null performance should be handled without crashing and not affect ratings."""
|
|
714
714
|
df_with_null = sample_df.with_columns(
|
|
715
715
|
pl.when(pl.col("pid") == "P1").then(None).otherwise(pl.col("perf")).alias("perf")
|
|
716
716
|
)
|
|
717
717
|
gen = PlayerRatingGenerator(performance_column="perf", column_names=base_cn)
|
|
718
718
|
|
|
719
|
-
# Depending on implementation, it might skip P1 or treat as 0.
|
|
720
|
-
# The key is that the generator shouldn't crash.
|
|
721
719
|
res = gen.fit_transform(df_with_null)
|
|
722
720
|
assert len(res) == 4
|
|
723
721
|
|
|
722
|
+
assert gen._player_off_ratings["P2"].rating_value < 1000.0
|
|
723
|
+
assert gen._player_off_ratings["P3"].rating_value > 1000.0
|
|
724
|
+
|
|
724
725
|
|
|
725
726
|
def test_fit_transform_null_performance__no_rating_change(base_cn):
|
|
726
727
|
"""Players with null performance should have zero rating change, not be treated as 0.0 perf."""
|
|
@@ -1706,7 +1707,6 @@ def test_player_rating_features_out_combinations(
|
|
|
1706
1707
|
)
|
|
1707
1708
|
result = gen.fit_transform(sample_df)
|
|
1708
1709
|
|
|
1709
|
-
# Check that all expected columns are present
|
|
1710
1710
|
result_cols = (
|
|
1711
1711
|
result.columns.tolist() if hasattr(result.columns, "tolist") else list(result.columns)
|
|
1712
1712
|
)
|
|
@@ -1715,93 +1715,17 @@ def test_player_rating_features_out_combinations(
|
|
|
1715
1715
|
col in result_cols
|
|
1716
1716
|
), f"Expected column '{col}' not found in output. Columns: {result_cols}"
|
|
1717
1717
|
|
|
1718
|
-
|
|
1719
|
-
assert len(result) > 0
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
@pytest.mark.parametrize("output_suffix", [None, "v2", "custom_suffix", "test123"])
|
|
1723
|
-
def test_player_rating_suffix_applied_to_all_features(base_cn, sample_df, output_suffix):
|
|
1724
|
-
"""Test that output_suffix is correctly applied to all requested features."""
|
|
1725
|
-
features = [
|
|
1726
|
-
RatingKnownFeatures.PLAYER_OFF_RATING,
|
|
1727
|
-
RatingKnownFeatures.PLAYER_DEF_RATING,
|
|
1728
|
-
RatingKnownFeatures.TEAM_OFF_RATING_PROJECTED,
|
|
1729
|
-
]
|
|
1730
|
-
non_predictor = [
|
|
1731
|
-
RatingUnknownFeatures.PLAYER_PREDICTED_OFF_PERFORMANCE,
|
|
1732
|
-
RatingUnknownFeatures.TEAM_RATING,
|
|
1733
|
-
]
|
|
1734
|
-
|
|
1735
|
-
gen = PlayerRatingGenerator(
|
|
1736
|
-
performance_column="perf",
|
|
1737
|
-
column_names=base_cn,
|
|
1738
|
-
auto_scale_performance=True,
|
|
1739
|
-
features_out=features,
|
|
1740
|
-
non_predictor_features_out=non_predictor,
|
|
1741
|
-
output_suffix=output_suffix,
|
|
1742
|
-
)
|
|
1743
|
-
result = gen.fit_transform(sample_df)
|
|
1744
|
-
|
|
1745
|
-
# Build expected column names
|
|
1746
|
-
if output_suffix:
|
|
1747
|
-
expected_cols = [
|
|
1748
|
-
f"player_off_rating_{output_suffix}",
|
|
1749
|
-
f"player_def_rating_{output_suffix}",
|
|
1750
|
-
f"team_off_rating_projected_{output_suffix}",
|
|
1751
|
-
f"player_predicted_off_performance_{output_suffix}",
|
|
1752
|
-
f"team_rating_{output_suffix}",
|
|
1753
|
-
]
|
|
1754
|
-
else:
|
|
1755
|
-
# When output_suffix=None, it defaults to performance column name ("perf")
|
|
1756
|
-
expected_cols = [
|
|
1757
|
-
"player_off_rating_perf",
|
|
1758
|
-
"player_def_rating_perf",
|
|
1759
|
-
"team_off_rating_projected_perf",
|
|
1760
|
-
"player_predicted_off_performance_perf",
|
|
1761
|
-
"team_rating_perf",
|
|
1762
|
-
]
|
|
1718
|
+
assert len(result) == 4
|
|
1763
1719
|
|
|
1764
|
-
result_cols = (
|
|
1765
|
-
result.columns.tolist() if hasattr(result.columns, "tolist") else list(result.columns)
|
|
1766
|
-
)
|
|
1767
1720
|
for col in expected_cols:
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
auto_scale_performance=True,
|
|
1777
|
-
features_out=[RatingKnownFeatures.PLAYER_OFF_RATING],
|
|
1778
|
-
non_predictor_features_out=None,
|
|
1779
|
-
output_suffix=None,
|
|
1780
|
-
)
|
|
1781
|
-
result = gen.fit_transform(sample_df)
|
|
1782
|
-
|
|
1783
|
-
# Should have input columns + requested feature
|
|
1784
|
-
input_cols = set(sample_df.columns)
|
|
1785
|
-
result_cols = set(result.columns)
|
|
1786
|
-
|
|
1787
|
-
# Check that input columns are preserved
|
|
1788
|
-
for col in input_cols:
|
|
1789
|
-
assert col in result_cols, f"Input column '{col}' missing from output"
|
|
1790
|
-
|
|
1791
|
-
# Check that requested feature is present (with performance column suffix)
|
|
1792
|
-
assert "player_off_rating_perf" in result_cols
|
|
1793
|
-
|
|
1794
|
-
# Check that other rating features are NOT present (unless they're input columns)
|
|
1795
|
-
unwanted_features = [
|
|
1796
|
-
"player_def_rating",
|
|
1797
|
-
"team_off_rating_projected",
|
|
1798
|
-
"player_predicted_off_performance",
|
|
1799
|
-
]
|
|
1800
|
-
for feature in unwanted_features:
|
|
1801
|
-
if feature not in input_cols:
|
|
1802
|
-
assert (
|
|
1803
|
-
feature not in result_cols
|
|
1804
|
-
), f"Unrequested feature '{feature}' should not be in output"
|
|
1721
|
+
if "rating" in col and "difference" not in col:
|
|
1722
|
+
values = result[col].to_list()
|
|
1723
|
+
for v in values:
|
|
1724
|
+
assert 500 < v < 1500, f"Rating {col}={v} outside reasonable range"
|
|
1725
|
+
elif "predicted" in col:
|
|
1726
|
+
values = result[col].to_list()
|
|
1727
|
+
for v in values:
|
|
1728
|
+
assert 0.0 <= v <= 1.0, f"Prediction {col}={v} outside [0,1]"
|
|
1805
1729
|
|
|
1806
1730
|
|
|
1807
1731
|
def test_player_rating_team_with_strong_offense_and_weak_defense_gets_expected_ratings_and_predictions(
|
|
@@ -2008,3 +1932,110 @@ def test_fit_transform__start_league_quantile_uses_existing_player_ratings(base_
|
|
|
2008
1932
|
f"but got {new_player_start_rating:.1f}. "
|
|
2009
1933
|
"start_league_quantile has no effect because update_players_to_leagues is never called."
|
|
2010
1934
|
)
|
|
1935
|
+
|
|
1936
|
+
|
|
1937
|
+
def test_fit_transform__precise_rating_calculation(base_cn, sample_df):
|
|
1938
|
+
"""Verify precise rating calculations match expected formulas."""
|
|
1939
|
+
gen = PlayerRatingGenerator(
|
|
1940
|
+
performance_column="perf",
|
|
1941
|
+
column_names=base_cn,
|
|
1942
|
+
auto_scale_performance=False,
|
|
1943
|
+
features_out=[RatingKnownFeatures.PLAYER_OFF_RATING],
|
|
1944
|
+
)
|
|
1945
|
+
gen.fit_transform(sample_df)
|
|
1946
|
+
|
|
1947
|
+
expected_mult = 50 * (17 / 14) * 0.9 + 5
|
|
1948
|
+
|
|
1949
|
+
assert gen._player_off_ratings["P1"].rating_value == pytest.approx(
|
|
1950
|
+
1000 + 0.1 * expected_mult, rel=1e-6
|
|
1951
|
+
)
|
|
1952
|
+
|
|
1953
|
+
assert gen._player_off_ratings["P2"].rating_value == pytest.approx(
|
|
1954
|
+
1000 - 0.1 * expected_mult, rel=1e-6
|
|
1955
|
+
)
|
|
1956
|
+
|
|
1957
|
+
assert gen._player_off_ratings["P3"].rating_value == pytest.approx(
|
|
1958
|
+
1000 + 0.2 * expected_mult, rel=1e-6
|
|
1959
|
+
)
|
|
1960
|
+
|
|
1961
|
+
assert gen._player_off_ratings["P4"].rating_value == pytest.approx(
|
|
1962
|
+
1000 - 0.2 * expected_mult, rel=1e-6
|
|
1963
|
+
)
|
|
1964
|
+
|
|
1965
|
+
assert gen._player_def_ratings["P1"].rating_value == pytest.approx(1000.0, rel=1e-6)
|
|
1966
|
+
|
|
1967
|
+
|
|
1968
|
+
def test_fit_transform_when_all_players_have_null_performance_then_no_rating_change(base_cn):
|
|
1969
|
+
"""
|
|
1970
|
+
When ALL players on a team have null performance, opponent defense ratings should not change.
|
|
1971
|
+
|
|
1972
|
+
Scenario:
|
|
1973
|
+
- Match 1: Normal match with performance values (P1=0.6 on T1, P2=0.4 on T2)
|
|
1974
|
+
- Match 2: T1 has ALL null performance, T2 has normal performance (0.6)
|
|
1975
|
+
|
|
1976
|
+
Expected:
|
|
1977
|
+
- T2 players' defensive rating should NOT change after M2 because T1's offensive
|
|
1978
|
+
performance is unknown (all null) - we can't evaluate how well T2 defended
|
|
1979
|
+
- T1 players' offensive rating should NOT change after M2 (null perf = no update)
|
|
1980
|
+
"""
|
|
1981
|
+
df = pl.DataFrame(
|
|
1982
|
+
{
|
|
1983
|
+
"pid": ["P1", "P2", "P3", "P4", "P1", "P2", "P3", "P4"],
|
|
1984
|
+
"tid": ["T1", "T1", "T2", "T2", "T1", "T1", "T2", "T2"],
|
|
1985
|
+
"mid": ["M1", "M1", "M1", "M1", "M2", "M2", "M2", "M2"],
|
|
1986
|
+
"dt": [
|
|
1987
|
+
"2024-01-01",
|
|
1988
|
+
"2024-01-01",
|
|
1989
|
+
"2024-01-01",
|
|
1990
|
+
"2024-01-01",
|
|
1991
|
+
"2024-01-02",
|
|
1992
|
+
"2024-01-02",
|
|
1993
|
+
"2024-01-02",
|
|
1994
|
+
"2024-01-02",
|
|
1995
|
+
],
|
|
1996
|
+
"perf": [0.6, 0.4, 0.6, 0.4, None, None, 0.6, 0.4],
|
|
1997
|
+
"pw": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
|
|
1998
|
+
}
|
|
1999
|
+
)
|
|
2000
|
+
|
|
2001
|
+
gen = PlayerRatingGenerator(
|
|
2002
|
+
performance_column="perf",
|
|
2003
|
+
column_names=base_cn,
|
|
2004
|
+
features_out=[
|
|
2005
|
+
RatingKnownFeatures.PLAYER_OFF_RATING,
|
|
2006
|
+
RatingKnownFeatures.PLAYER_DEF_RATING,
|
|
2007
|
+
],
|
|
2008
|
+
)
|
|
2009
|
+
result = gen.fit_transform(df)
|
|
2010
|
+
|
|
2011
|
+
p3_def_before_m2 = result.filter((pl.col("pid") == "P3") & (pl.col("mid") == "M2"))[
|
|
2012
|
+
"player_def_rating_perf"
|
|
2013
|
+
][0]
|
|
2014
|
+
p4_def_before_m2 = result.filter((pl.col("pid") == "P4") & (pl.col("mid") == "M2"))[
|
|
2015
|
+
"player_def_rating_perf"
|
|
2016
|
+
][0]
|
|
2017
|
+
|
|
2018
|
+
p3_def_after_m2 = gen._player_def_ratings["P3"].rating_value
|
|
2019
|
+
p4_def_after_m2 = gen._player_def_ratings["P4"].rating_value
|
|
2020
|
+
|
|
2021
|
+
assert p3_def_before_m2 == p3_def_after_m2, (
|
|
2022
|
+
f"P3's def rating changed after M2 with all-null T1 performance! "
|
|
2023
|
+
f"Before={p3_def_before_m2}, After={p3_def_after_m2}. "
|
|
2024
|
+
"T2 defense should not be evaluated when T1 offense is unknown."
|
|
2025
|
+
)
|
|
2026
|
+
assert p4_def_before_m2 == p4_def_after_m2, (
|
|
2027
|
+
f"P4's def rating changed after M2 with all-null T1 performance! "
|
|
2028
|
+
f"Before={p4_def_before_m2}, After={p4_def_after_m2}. "
|
|
2029
|
+
"T2 defense should not be evaluated when T1 offense is unknown."
|
|
2030
|
+
)
|
|
2031
|
+
|
|
2032
|
+
p1_off_before_m2 = result.filter((pl.col("pid") == "P1") & (pl.col("mid") == "M2"))[
|
|
2033
|
+
"player_off_rating_perf"
|
|
2034
|
+
][0]
|
|
2035
|
+
p1_off_after_m2 = gen._player_off_ratings["P1"].rating_value
|
|
2036
|
+
|
|
2037
|
+
assert p1_off_before_m2 == p1_off_after_m2, (
|
|
2038
|
+
f"P1's off rating changed after M2 with null performance! "
|
|
2039
|
+
f"Before={p1_off_before_m2}, After={p1_off_after_m2}. "
|
|
2040
|
+
"Null performance should result in no rating change."
|
|
2041
|
+
)
|
|
@@ -835,6 +835,149 @@ def test_fit_transform_when_performance_out_of_range_then_error_is_raised(column
|
|
|
835
835
|
generator.fit_transform(df)
|
|
836
836
|
|
|
837
837
|
|
|
838
|
+
def test_fit_transform_when_performance_is_null_then_no_rating_change(column_names):
|
|
839
|
+
"""
|
|
840
|
+
When performance is null, then we should expect to see no rating change
|
|
841
|
+
because null means missing data, not 0.0 (worst) performance.
|
|
842
|
+
The team's pre-match rating for the next game should equal their rating before the null game.
|
|
843
|
+
"""
|
|
844
|
+
generator = TeamRatingGenerator(
|
|
845
|
+
performance_column="won",
|
|
846
|
+
column_names=column_names,
|
|
847
|
+
start_team_rating=1000.0,
|
|
848
|
+
confidence_weight=0.0,
|
|
849
|
+
output_suffix="",
|
|
850
|
+
features_out=[RatingKnownFeatures.TEAM_OFF_RATING_PROJECTED],
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
# Match 1: team_a perf=0.6, team_b perf=0.4
|
|
854
|
+
# Match 2: team_a has null performance, team_b perf=0.6
|
|
855
|
+
# Match 3: team_a perf=0.6, team_b perf=0.4
|
|
856
|
+
df = pl.DataFrame(
|
|
857
|
+
{
|
|
858
|
+
"match_id": [1, 1, 2, 2, 3, 3],
|
|
859
|
+
"team_id": ["team_a", "team_b", "team_a", "team_b", "team_a", "team_b"],
|
|
860
|
+
"start_date": [
|
|
861
|
+
datetime(2024, 1, 1),
|
|
862
|
+
datetime(2024, 1, 1),
|
|
863
|
+
datetime(2024, 1, 2),
|
|
864
|
+
datetime(2024, 1, 2),
|
|
865
|
+
datetime(2024, 1, 3),
|
|
866
|
+
datetime(2024, 1, 3),
|
|
867
|
+
],
|
|
868
|
+
"won": [0.6, 0.4, None, 0.6, 0.6, 0.4], # team_a has null in match 2
|
|
869
|
+
}
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
result = generator.fit_transform(df)
|
|
873
|
+
|
|
874
|
+
# Get team_a's pre-match rating for match 2 (after match 1) and match 3 (after match 2)
|
|
875
|
+
team_a_rating_before_m2 = result.filter(
|
|
876
|
+
(pl.col("team_id") == "team_a") & (pl.col("match_id") == 2)
|
|
877
|
+
)["team_off_rating_projected"][0]
|
|
878
|
+
team_a_rating_before_m3 = result.filter(
|
|
879
|
+
(pl.col("team_id") == "team_a") & (pl.col("match_id") == 3)
|
|
880
|
+
)["team_off_rating_projected"][0]
|
|
881
|
+
|
|
882
|
+
# Key assertion: rating before M3 should equal rating before M2
|
|
883
|
+
# because null performance in M2 means NO rating change
|
|
884
|
+
assert team_a_rating_before_m3 == team_a_rating_before_m2, (
|
|
885
|
+
f"team_a's rating changed after null performance game! "
|
|
886
|
+
f"Before M2={team_a_rating_before_m2}, Before M3={team_a_rating_before_m3}"
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
# Also verify null is not treated as 0.0 by comparing with explicit 0.0
|
|
890
|
+
# Use 0.3 instead of 0.0 to keep mean in valid range
|
|
891
|
+
df_with_low_perf = df.with_columns(
|
|
892
|
+
pl.when((pl.col("team_id") == "team_a") & (pl.col("match_id") == 2))
|
|
893
|
+
.then(0.3) # Low performance (below predicted ~0.5) causes rating drop
|
|
894
|
+
.otherwise(pl.col("won"))
|
|
895
|
+
.alias("won")
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
gen_low = TeamRatingGenerator(
|
|
899
|
+
performance_column="won",
|
|
900
|
+
column_names=column_names,
|
|
901
|
+
start_team_rating=1000.0,
|
|
902
|
+
confidence_weight=0.0,
|
|
903
|
+
output_suffix="",
|
|
904
|
+
features_out=[RatingKnownFeatures.TEAM_OFF_RATING_PROJECTED],
|
|
905
|
+
)
|
|
906
|
+
result_low = gen_low.fit_transform(df_with_low_perf)
|
|
907
|
+
|
|
908
|
+
team_a_rating_before_m3_with_low = result_low.filter(
|
|
909
|
+
(pl.col("team_id") == "team_a") & (pl.col("match_id") == 3)
|
|
910
|
+
)["team_off_rating_projected"][0]
|
|
911
|
+
|
|
912
|
+
# With low perf (0.3), rating should drop (different from null which has no change)
|
|
913
|
+
assert team_a_rating_before_m3 > team_a_rating_before_m3_with_low, (
|
|
914
|
+
f"Null performance is being treated as low performance! "
|
|
915
|
+
f"Rating with null={team_a_rating_before_m3}, rating with low perf={team_a_rating_before_m3_with_low}"
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
def test_transform_when_auto_scale_performance_then_uses_correct_column(column_names):
|
|
920
|
+
"""
|
|
921
|
+
When auto_scale_performance=True, the performance manager renames the column
|
|
922
|
+
(e.g., 'won' -> 'performance__won'). Transform should still work by applying
|
|
923
|
+
the performance manager to transform the input data.
|
|
924
|
+
|
|
925
|
+
Bug: Currently transform doesn't apply the performance manager, causing
|
|
926
|
+
a column mismatch where it looks for 'performance__won' but data has 'won'.
|
|
927
|
+
This results in None being returned and defaulting to 0.0 performance.
|
|
928
|
+
"""
|
|
929
|
+
generator = TeamRatingGenerator(
|
|
930
|
+
performance_column="won",
|
|
931
|
+
column_names=column_names,
|
|
932
|
+
start_team_rating=1000.0,
|
|
933
|
+
confidence_weight=0.0,
|
|
934
|
+
output_suffix="",
|
|
935
|
+
auto_scale_performance=True,
|
|
936
|
+
)
|
|
937
|
+
|
|
938
|
+
# fit_transform with valid performance values
|
|
939
|
+
fit_df = pl.DataFrame(
|
|
940
|
+
{
|
|
941
|
+
"match_id": [1, 1],
|
|
942
|
+
"team_id": ["team_a", "team_b"],
|
|
943
|
+
"start_date": [datetime(2024, 1, 1), datetime(2024, 1, 1)],
|
|
944
|
+
"won": [0.6, 0.4],
|
|
945
|
+
}
|
|
946
|
+
)
|
|
947
|
+
generator.fit_transform(fit_df)
|
|
948
|
+
|
|
949
|
+
# After fit_transform, performance_column is changed to 'performance__won'
|
|
950
|
+
assert generator.performance_column == "performance__won", (
|
|
951
|
+
f"Expected performance_column to be 'performance__won' but got '{generator.performance_column}'"
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
team_a_rating_before = generator._team_off_ratings["team_a"].rating_value
|
|
955
|
+
|
|
956
|
+
# transform with same format data (original column name 'won')
|
|
957
|
+
# team_a has good performance (0.6 > predicted ~0.5), so rating should INCREASE
|
|
958
|
+
transform_df = pl.DataFrame(
|
|
959
|
+
{
|
|
960
|
+
"match_id": [2, 2],
|
|
961
|
+
"team_id": ["team_a", "team_b"],
|
|
962
|
+
"start_date": [datetime(2024, 1, 2), datetime(2024, 1, 2)],
|
|
963
|
+
"won": [0.6, 0.4], # Original column name, not 'performance__won'
|
|
964
|
+
}
|
|
965
|
+
)
|
|
966
|
+
|
|
967
|
+
generator.transform(transform_df)
|
|
968
|
+
|
|
969
|
+
team_a_rating_after = generator._team_off_ratings["team_a"].rating_value
|
|
970
|
+
|
|
971
|
+
# With 0.6 performance (above predicted ~0.5), rating should INCREASE
|
|
972
|
+
# Bug: column mismatch causes perf to default to 0.0, making rating DECREASE
|
|
973
|
+
assert team_a_rating_after > team_a_rating_before, (
|
|
974
|
+
f"Rating should increase with good performance (0.6), but it went from "
|
|
975
|
+
f"{team_a_rating_before} to {team_a_rating_after}. This indicates transform "
|
|
976
|
+
f"is not finding the performance column (looking for '{generator.performance_column}' "
|
|
977
|
+
f"but data has 'won') and defaulting to 0.0 performance."
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
|
|
838
981
|
@pytest.mark.parametrize("confidence_weight", [0.0, 0.5, 1.0])
|
|
839
982
|
def test_fit_transform_when_confidence_weight_varies_then_new_teams_have_different_rating_changes(
|
|
840
983
|
column_names, confidence_weight
|
|
@@ -1283,12 +1426,11 @@ def test_transform_when_called_after_fit_transform_then_uses_updated_ratings(
|
|
|
1283
1426
|
assert team_a_row["team_off_rating_projected"][0] == pytest.approx(team_a_rating_after_first)
|
|
1284
1427
|
|
|
1285
1428
|
|
|
1286
|
-
def
|
|
1429
|
+
def test_transform_when_called_without_performance_column_then_no_rating_change(column_names):
|
|
1287
1430
|
"""
|
|
1288
1431
|
When transform is called without performance column, then we should expect to see
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
This means ratings will be updated as if teams lost (performance=0.0).
|
|
1432
|
+
ratings remain unchanged because null/missing performance means no rating update
|
|
1433
|
+
(not treated as 0.0 which would cause a rating drop).
|
|
1292
1434
|
"""
|
|
1293
1435
|
generator = TeamRatingGenerator(
|
|
1294
1436
|
performance_column="won",
|
|
@@ -1321,7 +1463,8 @@ def test_transform_when_called_without_performance_column_then_defaults_to_zero(
|
|
|
1321
1463
|
generator.transform(df)
|
|
1322
1464
|
team_a_rating_after = generator._team_off_ratings["team_a"].rating_value
|
|
1323
1465
|
|
|
1324
|
-
|
|
1466
|
+
# Null/missing performance means no rating change
|
|
1467
|
+
assert team_a_rating_after == team_a_rating_before
|
|
1325
1468
|
|
|
1326
1469
|
|
|
1327
1470
|
def test_future_transform_when_called_then_ratings_not_updated(basic_rating_generator):
|
|
@@ -1539,14 +1682,13 @@ def test_transform_vs_future_transform_when_same_match_then_transform_updates_ra
|
|
|
1539
1682
|
assert team_a_rating_after_future == team_a_rating_before_2
|
|
1540
1683
|
|
|
1541
1684
|
|
|
1542
|
-
def
|
|
1685
|
+
def test_transform_vs_future_transform_when_performance_column_missing_then_both_work_with_no_rating_change(
|
|
1543
1686
|
column_names,
|
|
1544
1687
|
):
|
|
1545
1688
|
"""
|
|
1546
1689
|
When performance column is missing, then we should expect to see
|
|
1547
|
-
both future_transform and transform work,
|
|
1548
|
-
|
|
1549
|
-
at all since it only computes predictions.
|
|
1690
|
+
both future_transform and transform work, and both result in no rating change
|
|
1691
|
+
because null/missing performance means no update (not treated as 0.0).
|
|
1550
1692
|
"""
|
|
1551
1693
|
generator = TeamRatingGenerator(
|
|
1552
1694
|
performance_column="won",
|
|
@@ -1594,8 +1736,9 @@ def test_transform_vs_future_transform_when_performance_column_missing_then_both
|
|
|
1594
1736
|
result_transform = generator.transform(transform_df)
|
|
1595
1737
|
assert result_transform is not None
|
|
1596
1738
|
|
|
1739
|
+
# Null/missing performance means no rating change
|
|
1597
1740
|
team_a_rating_after_transform = generator._team_off_ratings["team_a"].rating_value
|
|
1598
|
-
assert team_a_rating_after_transform
|
|
1741
|
+
assert team_a_rating_after_transform == team_a_rating_before
|
|
1599
1742
|
|
|
1600
1743
|
|
|
1601
1744
|
def test_transform_vs_future_transform_when_games_played_then_transform_increments_but_future_transform_does_not(
|
|
@@ -1878,7 +2021,6 @@ def test_transform_when_date_formats_vary_then_processes_successfully(column_nam
|
|
|
1878
2021
|
start_team_rating=1000.0,
|
|
1879
2022
|
confidence_weight=0.0,
|
|
1880
2023
|
output_suffix="",
|
|
1881
|
-
auto_scale_performance=True,
|
|
1882
2024
|
)
|
|
1883
2025
|
|
|
1884
2026
|
fit_df = pl.DataFrame(
|
|
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
|
|
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
|
|
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
|
{spforge-0.8.20 → spforge-0.8.23}/spforge/distributions/_student_t_distribution_estimator.py
RENAMED
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spforge-0.8.20 → spforge-0.8.23}/spforge/performance_transformers/_performances_transformers.py
RENAMED
|
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
|
|
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
|
|
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
|
|
File without changes
|
{spforge-0.8.20 → spforge-0.8.23}/tests/end_to_end/test_nba_player_ratings_hyperparameter_tuning.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spforge-0.8.20 → spforge-0.8.23}/tests/feature_generator/test_regressor_feature_generator.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spforge-0.8.20 → spforge-0.8.23}/tests/performance_transformers/test_performance_manager.py
RENAMED
|
File without changes
|
{spforge-0.8.20 → spforge-0.8.23}/tests/performance_transformers/test_performances_transformers.py
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|