spforge 0.8.8__py3-none-any.whl → 0.8.19__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.
- spforge/autopipeline.py +169 -5
- spforge/estimator/_group_by_estimator.py +11 -3
- spforge/hyperparameter_tuning/__init__.py +2 -0
- spforge/hyperparameter_tuning/_default_search_spaces.py +38 -23
- spforge/hyperparameter_tuning/_tuner.py +55 -2
- spforge/performance_transformers/_performance_manager.py +2 -4
- spforge/ratings/_player_rating.py +131 -28
- spforge/ratings/start_rating_generator.py +1 -1
- spforge/ratings/team_start_rating_generator.py +1 -1
- spforge/ratings/utils.py +16 -6
- spforge/scorer/_score.py +42 -11
- spforge/transformers/_other_transformer.py +38 -8
- {spforge-0.8.8.dist-info → spforge-0.8.19.dist-info}/METADATA +1 -1
- {spforge-0.8.8.dist-info → spforge-0.8.19.dist-info}/RECORD +25 -23
- {spforge-0.8.8.dist-info → spforge-0.8.19.dist-info}/WHEEL +1 -1
- tests/end_to_end/test_nba_player_ratings_hyperparameter_tuning.py +0 -4
- tests/hyperparameter_tuning/test_rating_tuner.py +157 -0
- tests/performance_transformers/test_performance_manager.py +15 -0
- tests/ratings/test_player_rating_generator.py +127 -0
- tests/ratings/test_player_rating_no_mutation.py +214 -0
- tests/ratings/test_utils_scaled_weights.py +136 -0
- tests/scorer/test_score.py +142 -0
- tests/test_autopipeline.py +336 -6
- {spforge-0.8.8.dist-info → spforge-0.8.19.dist-info}/licenses/LICENSE +0 -0
- {spforge-0.8.8.dist-info → spforge-0.8.19.dist-info}/top_level.txt +0 -0
|
@@ -14,7 +14,7 @@ examples/nba/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
|
|
|
14
14
|
examples/nba/data/game_player_subsample.parquet,sha256=ODJxHC-mUYbJ7r-ScUFtPU7hrFuxLUbbDSobmpCkw0w,279161
|
|
15
15
|
examples/nba/data/utils.py,sha256=41hxLQ1d6ZgBEcHa5MI0-fG5KbsRi07cclMPQZM95ek,509
|
|
16
16
|
spforge/__init__.py,sha256=8vZhy7XUpzqWkVKpXqwqOLDkQlNytRhyf4qjwObfXgU,468
|
|
17
|
-
spforge/autopipeline.py,sha256=
|
|
17
|
+
spforge/autopipeline.py,sha256=rZ6FhJxcgNLvtr3hTVkEiW4BiorgXxADThfMuQ42orE,29866
|
|
18
18
|
spforge/base_feature_generator.py,sha256=RbD00N6oLCQQcEb_VF5wbwZztl-X8k9B0Wlaj9Os1iU,668
|
|
19
19
|
spforge/data_structures.py,sha256=k82v5r79vl0_FAVvsxVF9Nbzb5FoHqVrlHZlEXGc5gQ,7298
|
|
20
20
|
spforge/features_generator_pipeline.py,sha256=n8vzZKqXNFcFRDWZhllnkhAh5NFXdOD3FEIOpHcay8E,8208
|
|
@@ -30,7 +30,7 @@ spforge/estimator/__init__.py,sha256=zIJ4u7WGPOALPx8kVBppBOqklI4lQPl9QBWT8JjjFoY
|
|
|
30
30
|
spforge/estimator/_conditional_estimator.py,sha256=JSHpOg5lv3kRv_VzSZ0fKbwCO2dJv9XpyLs9lS81psU,4904
|
|
31
31
|
spforge/estimator/_frequency_bucketing_classifier.py,sha256=d7wDpOCoKWf-WoXtzwahjtmAozkFdKE3-pzs477WMYc,6055
|
|
32
32
|
spforge/estimator/_granularity_estimator.py,sha256=pUNmtpDFoOVbS9mHfO-zvidPIKJgWts0y2VnhJ8VWww,3829
|
|
33
|
-
spforge/estimator/_group_by_estimator.py,sha256=
|
|
33
|
+
spforge/estimator/_group_by_estimator.py,sha256=o-xv_PJJyWBaKv5Eo4EPbOvb9i0CuebZnX4GtEFp_Js,3120
|
|
34
34
|
spforge/estimator/_ordinal_classifier.py,sha256=j_dfVHeX-6eZgPwwsYbkbP6bPrKH2a5S-N8vfP5hneA,1993
|
|
35
35
|
spforge/estimator/_sklearn_enhancer_estimator.py,sha256=DZ-UlmeazXPd6uEnlbVv79syZ5FPa64voUyKArtjjUs,4664
|
|
36
36
|
spforge/feature_generator/__init__.py,sha256=wfLfUkC_lLOCpy7NgDytK-l3HUAuhikuQXdKCgSGbuA,556
|
|
@@ -43,36 +43,36 @@ spforge/feature_generator/_rolling_mean_binary.py,sha256=lmODy-o9Dd9pb8IlA7g4UyA
|
|
|
43
43
|
spforge/feature_generator/_rolling_mean_days.py,sha256=EZQmFmYVQB-JjZV5k8bOWnaTxNpPDCZAjdfdhiiG4r4,8415
|
|
44
44
|
spforge/feature_generator/_rolling_window.py,sha256=HT8LezsRIPNAlMEoP9oTPW2bKFu55ZSRnQZGST7fncw,8836
|
|
45
45
|
spforge/feature_generator/_utils.py,sha256=KDn33ia1OYJTK8THFpvc_uRiH_Bl3fImGqqbfzs0YA4,9654
|
|
46
|
-
spforge/hyperparameter_tuning/__init__.py,sha256=
|
|
47
|
-
spforge/hyperparameter_tuning/_default_search_spaces.py,sha256=
|
|
48
|
-
spforge/hyperparameter_tuning/_tuner.py,sha256=
|
|
46
|
+
spforge/hyperparameter_tuning/__init__.py,sha256=Vcl8rVlJ7M708iPgqe4XxpZWgJKGux0Y5HgMCymRsHg,1099
|
|
47
|
+
spforge/hyperparameter_tuning/_default_search_spaces.py,sha256=SjwXLpvYIu_JY8uPRHeL5Kgp1aa0slWDz8qsKDaohWQ,8020
|
|
48
|
+
spforge/hyperparameter_tuning/_tuner.py,sha256=M79q3saM6r0UZJsRUUgfdDr-3Qii-F2-wuSAZLFtZDo,19246
|
|
49
49
|
spforge/performance_transformers/__init__.py,sha256=U6d7_kltbUMLYCGBk4QAFVPJTxXD3etD9qUftV-O3q4,422
|
|
50
|
-
spforge/performance_transformers/_performance_manager.py,sha256=
|
|
50
|
+
spforge/performance_transformers/_performance_manager.py,sha256=WmjmlMEnq7y75MiI_s9Y-9eMXIyhPTUKrwsXRtgYp0k,9620
|
|
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=dRMkIGj5-2zKddygaEA4g16WCyXon7v8Xa1ymm7IuoM,14335
|
|
54
|
-
spforge/ratings/_player_rating.py,sha256=
|
|
54
|
+
spforge/ratings/_player_rating.py,sha256=JSTXdaRw_b8ZoZxgmMnZrYG7gPg8GKawqalLd16SK1M,56066
|
|
55
55
|
spforge/ratings/_team_rating.py,sha256=T0kFiv3ykYSrVGGsVRa8ZxLB0WMnagxqdFDzl9yZ_9g,24813
|
|
56
56
|
spforge/ratings/enums.py,sha256=s7z_RcZS6Nlgfa_6tasO8_IABZJwywexe7sep9DJBgo,1739
|
|
57
57
|
spforge/ratings/league_identifier.py,sha256=_KDUKOwoNU6RNFKE5jju4eYFGVNGBdJsv5mhNvMakfc,6019
|
|
58
58
|
spforge/ratings/league_start_rating_optimizer.py,sha256=Q4Vo3QT-r55qP4aD9WftsTB00UOSRvxM1khlyuAGWNM,8582
|
|
59
59
|
spforge/ratings/player_performance_predictor.py,sha256=cMxzQuk0nF1MsT_M32g-3mxVdAEbZ-S7TUjEPYdo3Yg,8361
|
|
60
|
-
spforge/ratings/start_rating_generator.py,sha256=
|
|
60
|
+
spforge/ratings/start_rating_generator.py,sha256=eSasa5Oe9n4IoTGjFCYyFQAGrJtzrBW-Qor97lmaYuM,6776
|
|
61
61
|
spforge/ratings/team_performance_predictor.py,sha256=ThQOmYQUqKBB46ONYHOMM2arXFH8AkyKpAZzs80SjHA,7217
|
|
62
|
-
spforge/ratings/team_start_rating_generator.py,sha256=
|
|
63
|
-
spforge/ratings/utils.py,sha256=
|
|
62
|
+
spforge/ratings/team_start_rating_generator.py,sha256=vK-_m8KwcHopchch_lKNHSGLiiNm5q9Lenm0d1cP_po,5110
|
|
63
|
+
spforge/ratings/utils.py,sha256=_zFemqz2jJkH8rn2EZpDt8N6FELUmYp9qCnPzRtOIGU,4497
|
|
64
64
|
spforge/scorer/__init__.py,sha256=wj8PCvYIl6742Xwmt86c3oy6iqE8Ss-OpwHud6kd9IY,256
|
|
65
|
-
spforge/scorer/_score.py,sha256=
|
|
65
|
+
spforge/scorer/_score.py,sha256=kNuqiK3F5mUEAVD7KjWYY7E_AkRrspR362QBm_jyElg,57623
|
|
66
66
|
spforge/transformers/__init__.py,sha256=IPCsMcsgBqG52d0ttATLCY4HvFCQZddExlLt74U-zuI,390
|
|
67
67
|
spforge/transformers/_base.py,sha256=-smr_McQF9bYxM5-Agx6h7Xv_fhZzPfpAdQV-qK18bs,1134
|
|
68
68
|
spforge/transformers/_net_over_predicted.py,sha256=5dC8pvA1DNO0yXPSgJSMGU8zAHi-maUELm7FqFQVo-U,2321
|
|
69
69
|
spforge/transformers/_operator.py,sha256=jOH7wdMBLg6R2hlH_FU6eA0gjs-Q0vFimTo7fXgKpjI,2964
|
|
70
|
-
spforge/transformers/_other_transformer.py,sha256=
|
|
70
|
+
spforge/transformers/_other_transformer.py,sha256=w2a7Wnki3vJe4GAkSa4kealw0GILIo6nE_9-3M10owA,4646
|
|
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.
|
|
75
|
-
tests/test_autopipeline.py,sha256=
|
|
74
|
+
spforge-0.8.19.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
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
|
|
78
78
|
tests/cross_validator/test_cross_validator.py,sha256=itCGhNY8-NbDbKbhxHW20wiLuRst7-Rixpmi3FSKQtA,17474
|
|
@@ -81,7 +81,7 @@ tests/end_to_end/test_estimator_hyperparameter_tuning.py,sha256=fZCJ9rrED2vT68B9
|
|
|
81
81
|
tests/end_to_end/test_league_start_rating_optimizer.py,sha256=Mmct2ixp4c6L7PGym8wZc7E-Csozryt1g4_o6OCc1uI,3141
|
|
82
82
|
tests/end_to_end/test_lol_player_kills.py,sha256=RJSYUbPrZ-RzSxGggj03yN0JKYeTB1JghVGYFMYia3Y,11891
|
|
83
83
|
tests/end_to_end/test_nba_player_points.py,sha256=kyzjo7QIcvpteps29Wix6IS_eJG9d1gHLeWtIHpkWMs,9066
|
|
84
|
-
tests/end_to_end/test_nba_player_ratings_hyperparameter_tuning.py,sha256=
|
|
84
|
+
tests/end_to_end/test_nba_player_ratings_hyperparameter_tuning.py,sha256=0lI4Xtg3V-zmo6prgzdNG80yy7JjvFVO-J_OU0pljyc,6346
|
|
85
85
|
tests/end_to_end/test_nba_prediction_consistency.py,sha256=o3DckJasx_I1ed6MhMYZUo2WSDvQ_p3HtJa9DCWTIYU,9857
|
|
86
86
|
tests/estimator/test_sklearn_estimator.py,sha256=tVfOP9Wx-tV1b6DcHbGxQHZQzNPA0Iobq8jTcUrk59U,48668
|
|
87
87
|
tests/feature_generator/test_lag.py,sha256=5Ffrv0V9cwkbkzRMPBe3_c_YNW-W2al-XH_acQIvdeg,19531
|
|
@@ -91,13 +91,15 @@ tests/feature_generator/test_rolling_mean_binary.py,sha256=KuIavJ37Pt8icAb50B23l
|
|
|
91
91
|
tests/feature_generator/test_rolling_mean_days.py,sha256=EyOvdJDnmgPfe13uQBOkwo7fAteBQx-tnyuGM4ng2T8,18884
|
|
92
92
|
tests/feature_generator/test_rolling_window.py,sha256=YBJo36OK3ILYeXrH06ylXqviUcCaGYaVQaK5RJzwM7Y,23239
|
|
93
93
|
tests/hyperparameter_tuning/test_estimator_tuner.py,sha256=iewME41d6LR2aQ0OtohGFtN_ocJUwTeqvs6L0QDmfG4,4413
|
|
94
|
-
tests/hyperparameter_tuning/test_rating_tuner.py,sha256=
|
|
95
|
-
tests/performance_transformers/test_performance_manager.py,sha256=
|
|
94
|
+
tests/hyperparameter_tuning/test_rating_tuner.py,sha256=usjC2ioO_yWRjjNAlRTyMVYheOrCi0kKocmHQHdTmpM,18699
|
|
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=SKLaBQBsHYslc2Nia2AxZ8A9Cy16MbZAWjLyOjvcMnA,64094
|
|
98
|
+
tests/ratings/test_player_rating_no_mutation.py,sha256=GzO3Hl__5K68DS3uRLefwnbcTJOvBM7cZqww4M21UZM,8493
|
|
98
99
|
tests/ratings/test_ratings_property.py,sha256=ckyfGILXa4tfQvsgyXEzBDNr2DUmHwFRV13N60w66iE,6561
|
|
99
100
|
tests/ratings/test_team_rating_generator.py,sha256=cDnf1zHiYC7pkgydE3MYr8wSTJIq-bPfSqhIRI_4Tic,95357
|
|
100
|
-
tests/
|
|
101
|
+
tests/ratings/test_utils_scaled_weights.py,sha256=iHxe6ZDUB_I2B6HT0xTGqXBkl7gRlqVV0e_7Lwun5po,4988
|
|
102
|
+
tests/scorer/test_score.py,sha256=rw3xJs6xqWVpalVMUQz557m2JYGR7PmhrsjfTex0b0c,79121
|
|
101
103
|
tests/scorer/test_score_aggregation_granularity.py,sha256=h-hyFOLzwp-92hYVU7CwvlRJ8jhB4DzXCtqgI-zcoqM,13677
|
|
102
104
|
tests/transformers/test_estimator_transformer_context.py,sha256=5GOHbuWCWBMFwwOTJOuD4oNDsv-qDR0OxNZYGGuMdag,1819
|
|
103
105
|
tests/transformers/test_net_over_predicted.py,sha256=vh7O1iRRPf4vcW9aLhOMAOyatfM5ZnLsQBKNAYsR3SU,3363
|
|
@@ -105,7 +107,7 @@ tests/transformers/test_other_transformer.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRk
|
|
|
105
107
|
tests/transformers/test_predictor_transformer.py,sha256=N1aBYLjN3ldpYZLwjih_gTFYSMitrZu-PNK78W6RHaQ,6877
|
|
106
108
|
tests/transformers/test_simple_transformer.py,sha256=wWR0qjLb_uS4HXrJgGdiqugOY1X7kwd1_OPS02IT2b8,4676
|
|
107
109
|
tests/transformers/test_team_ratio_predictor.py,sha256=fOUP_JvNJi-3kom3ZOs1EdG0I6Z8hpLpYKNHu1eWtOw,8562
|
|
108
|
-
spforge-0.8.
|
|
109
|
-
spforge-0.8.
|
|
110
|
-
spforge-0.8.
|
|
111
|
-
spforge-0.8.
|
|
110
|
+
spforge-0.8.19.dist-info/METADATA,sha256=4q1uKNTzmI9bwRwMJQaM0N6SAaC1RDembf_Gfbm2-mw,20048
|
|
111
|
+
spforge-0.8.19.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
112
|
+
spforge-0.8.19.dist-info/top_level.txt,sha256=6UW2M5a7WKOeaAi900qQmRKNj5-HZzE8-eUD9Y9LTq0,23
|
|
113
|
+
spforge-0.8.19.dist-info/RECORD,,
|
|
@@ -96,12 +96,8 @@ def test_nba_player_ratings_hyperparameter_tuning__workflow_completes(
|
|
|
96
96
|
"confidence_value_denom",
|
|
97
97
|
"confidence_max_sum",
|
|
98
98
|
"use_off_def_split",
|
|
99
|
-
"performance_predictor",
|
|
100
|
-
"start_team_weight",
|
|
101
99
|
"start_league_quantile",
|
|
102
100
|
"start_min_count_for_percentiles",
|
|
103
|
-
"start_min_match_count_team_rating",
|
|
104
|
-
"start_team_rating_subtract",
|
|
105
101
|
}
|
|
106
102
|
assert set(result.best_params.keys()) == expected_params
|
|
107
103
|
|
|
@@ -454,3 +454,160 @@ def test_param_spec__categorical_requires_choices():
|
|
|
454
454
|
|
|
455
455
|
with pytest.raises(ValueError, match="requires choices"):
|
|
456
456
|
spec.suggest(trial, "test_param")
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def test_param_ranges__overrides_bounds(
|
|
460
|
+
player_rating_generator, cross_validator, scorer, sample_player_df_pd
|
|
461
|
+
):
|
|
462
|
+
"""Test that param_ranges overrides low/high bounds while preserving param_type."""
|
|
463
|
+
tuner = RatingHyperparameterTuner(
|
|
464
|
+
rating_generator=player_rating_generator,
|
|
465
|
+
cross_validator=cross_validator,
|
|
466
|
+
scorer=scorer,
|
|
467
|
+
direction="minimize",
|
|
468
|
+
param_ranges={
|
|
469
|
+
"confidence_weight": (0.2, 0.3),
|
|
470
|
+
},
|
|
471
|
+
n_trials=3,
|
|
472
|
+
show_progress_bar=False,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
result = tuner.optimize(sample_player_df_pd)
|
|
476
|
+
|
|
477
|
+
assert "confidence_weight" in result.best_params
|
|
478
|
+
assert 0.2 <= result.best_params["confidence_weight"] <= 0.3
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def test_exclude_params__removes_from_search(
|
|
482
|
+
player_rating_generator, cross_validator, scorer, sample_player_df_pd
|
|
483
|
+
):
|
|
484
|
+
"""Test that exclude_params removes parameters from search space."""
|
|
485
|
+
tuner = RatingHyperparameterTuner(
|
|
486
|
+
rating_generator=player_rating_generator,
|
|
487
|
+
cross_validator=cross_validator,
|
|
488
|
+
scorer=scorer,
|
|
489
|
+
direction="minimize",
|
|
490
|
+
exclude_params=["use_off_def_split", "confidence_weight"],
|
|
491
|
+
n_trials=3,
|
|
492
|
+
show_progress_bar=False,
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
result = tuner.optimize(sample_player_df_pd)
|
|
496
|
+
|
|
497
|
+
assert "use_off_def_split" not in result.best_params
|
|
498
|
+
assert "confidence_weight" not in result.best_params
|
|
499
|
+
assert "rating_change_multiplier_offense" in result.best_params
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def test_fixed_params__applies_values_without_tuning(
|
|
503
|
+
player_rating_generator, cross_validator, scorer, sample_player_df_pd
|
|
504
|
+
):
|
|
505
|
+
"""Test that fixed_params sets values without including in search space."""
|
|
506
|
+
tuner = RatingHyperparameterTuner(
|
|
507
|
+
rating_generator=player_rating_generator,
|
|
508
|
+
cross_validator=cross_validator,
|
|
509
|
+
scorer=scorer,
|
|
510
|
+
direction="minimize",
|
|
511
|
+
fixed_params={"use_off_def_split": False},
|
|
512
|
+
n_trials=3,
|
|
513
|
+
show_progress_bar=False,
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
result = tuner.optimize(sample_player_df_pd)
|
|
517
|
+
|
|
518
|
+
assert "use_off_def_split" not in result.best_params
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def test_param_ranges__unknown_param_raises_error(
|
|
522
|
+
player_rating_generator, cross_validator, scorer, sample_player_df_pd
|
|
523
|
+
):
|
|
524
|
+
"""Test that param_ranges with unknown param raises ValueError."""
|
|
525
|
+
tuner = RatingHyperparameterTuner(
|
|
526
|
+
rating_generator=player_rating_generator,
|
|
527
|
+
cross_validator=cross_validator,
|
|
528
|
+
scorer=scorer,
|
|
529
|
+
direction="minimize",
|
|
530
|
+
param_ranges={"nonexistent_param": (0.0, 1.0)},
|
|
531
|
+
n_trials=3,
|
|
532
|
+
show_progress_bar=False,
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
with pytest.raises(ValueError, match="unknown parameter"):
|
|
536
|
+
tuner.optimize(sample_player_df_pd)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def test_param_ranges__non_numeric_param_raises_error(
|
|
540
|
+
player_rating_generator, cross_validator, scorer, sample_player_df_pd
|
|
541
|
+
):
|
|
542
|
+
"""Test that param_ranges on non-float/int param raises ValueError."""
|
|
543
|
+
tuner = RatingHyperparameterTuner(
|
|
544
|
+
rating_generator=player_rating_generator,
|
|
545
|
+
cross_validator=cross_validator,
|
|
546
|
+
scorer=scorer,
|
|
547
|
+
direction="minimize",
|
|
548
|
+
param_ranges={"use_off_def_split": (0, 1)},
|
|
549
|
+
n_trials=3,
|
|
550
|
+
show_progress_bar=False,
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
with pytest.raises(ValueError, match="can only override float/int"):
|
|
554
|
+
tuner.optimize(sample_player_df_pd)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def test_combined_api__param_ranges_exclude_fixed(
|
|
558
|
+
player_rating_generator, cross_validator, scorer, sample_player_df_pd
|
|
559
|
+
):
|
|
560
|
+
"""Test using param_ranges, exclude_params, and fixed_params together."""
|
|
561
|
+
tuner = RatingHyperparameterTuner(
|
|
562
|
+
rating_generator=player_rating_generator,
|
|
563
|
+
cross_validator=cross_validator,
|
|
564
|
+
scorer=scorer,
|
|
565
|
+
direction="minimize",
|
|
566
|
+
param_ranges={
|
|
567
|
+
"confidence_weight": (0.2, 1.0),
|
|
568
|
+
"rating_change_multiplier_offense": (10.0, 150.0),
|
|
569
|
+
},
|
|
570
|
+
exclude_params=["start_league_quantile"],
|
|
571
|
+
fixed_params={"use_off_def_split": False},
|
|
572
|
+
n_trials=3,
|
|
573
|
+
show_progress_bar=False,
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
result = tuner.optimize(sample_player_df_pd)
|
|
577
|
+
|
|
578
|
+
assert 0.2 <= result.best_params["confidence_weight"] <= 1.0
|
|
579
|
+
assert 10.0 <= result.best_params["rating_change_multiplier_offense"] <= 150.0
|
|
580
|
+
assert "start_league_quantile" not in result.best_params
|
|
581
|
+
assert "use_off_def_split" not in result.best_params
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def test_default_search_space__excludes_performance_predictor_and_team_start(
|
|
585
|
+
player_rating_generator,
|
|
586
|
+
):
|
|
587
|
+
"""Test that performance_predictor and team start params are not in default search space."""
|
|
588
|
+
from spforge.hyperparameter_tuning._default_search_spaces import (
|
|
589
|
+
get_default_search_space,
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
defaults = get_default_search_space(player_rating_generator)
|
|
593
|
+
|
|
594
|
+
assert "performance_predictor" not in defaults
|
|
595
|
+
assert "start_team_rating_subtract" not in defaults
|
|
596
|
+
assert "start_team_weight" not in defaults
|
|
597
|
+
assert "start_min_match_count_team_rating" not in defaults
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def test_full_player_rating_search_space__includes_all_params():
|
|
601
|
+
"""Test that full search space includes performance_predictor and team start params."""
|
|
602
|
+
from spforge.hyperparameter_tuning._default_search_spaces import (
|
|
603
|
+
get_full_player_rating_search_space,
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
full = get_full_player_rating_search_space()
|
|
607
|
+
|
|
608
|
+
assert "performance_predictor" in full
|
|
609
|
+
assert "start_team_rating_subtract" in full
|
|
610
|
+
assert "start_team_weight" in full
|
|
611
|
+
assert "start_min_match_count_team_rating" in full
|
|
612
|
+
assert "rating_change_multiplier_offense" in full
|
|
613
|
+
assert "confidence_weight" in full
|
|
@@ -56,6 +56,21 @@ def test_performance_weights_manager_basic_flow(sample_data):
|
|
|
56
56
|
assert output_df["weighted_performance"].iloc[0] == pytest.approx(0.6)
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
def test_performance_weights_manager_keeps_mean_when_weights_not_normalized():
|
|
60
|
+
df = pd.DataFrame(
|
|
61
|
+
{
|
|
62
|
+
"feat_a": [0.0, 1.0, 2.0, 3.0],
|
|
63
|
+
"feat_b": [3.0, 2.0, 1.0, 0.0],
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
weights = [ColumnWeight(name="feat_a", weight=0.9), ColumnWeight(name="feat_b", weight=0.5)]
|
|
67
|
+
|
|
68
|
+
manager = PerformanceWeightsManager(weights=weights, transformer_names=["min_max"], prefix="")
|
|
69
|
+
output_df = nw.from_native(manager.fit_transform(df)).to_pandas()
|
|
70
|
+
|
|
71
|
+
assert output_df["weighted_performance"].mean() == pytest.approx(0.5, abs=1e-6)
|
|
72
|
+
|
|
73
|
+
|
|
59
74
|
def test_lower_is_better_logic():
|
|
60
75
|
df = pd.DataFrame({"feat_a": [1.0, 0.0]})
|
|
61
76
|
weights = [ColumnWeight(name="feat_a", weight=1.0, lower_is_better=True)]
|
|
@@ -551,6 +551,63 @@ def test_fit_transform_scales_participation_weight_by_fit_quantile(base_cn):
|
|
|
551
551
|
assert p1_change / p2_change == pytest.approx(expected_ratio, rel=1e-6)
|
|
552
552
|
|
|
553
553
|
|
|
554
|
+
def test_fit_transform_auto_scales_participation_weight_when_out_of_bounds(base_cn):
|
|
555
|
+
"""Automatically enable scaling when participation weights exceed [0, 1]."""
|
|
556
|
+
df = pl.DataFrame(
|
|
557
|
+
{
|
|
558
|
+
"pid": ["P1", "P2", "O1", "O2"],
|
|
559
|
+
"tid": ["T1", "T1", "T2", "T2"],
|
|
560
|
+
"mid": ["M1", "M1", "M1", "M1"],
|
|
561
|
+
"dt": ["2024-01-01"] * 4,
|
|
562
|
+
"perf": [0.9, 0.9, 0.1, 0.1],
|
|
563
|
+
"pw": [10.0, 20.0, 10.0, 10.0],
|
|
564
|
+
}
|
|
565
|
+
)
|
|
566
|
+
gen = PlayerRatingGenerator(
|
|
567
|
+
performance_column="perf",
|
|
568
|
+
column_names=base_cn,
|
|
569
|
+
auto_scale_performance=True,
|
|
570
|
+
start_harcoded_start_rating=1000.0,
|
|
571
|
+
)
|
|
572
|
+
gen.fit_transform(df)
|
|
573
|
+
|
|
574
|
+
start_rating = 1000.0
|
|
575
|
+
p1_change = gen._player_off_ratings["P1"].rating_value - start_rating
|
|
576
|
+
p2_change = gen._player_off_ratings["P2"].rating_value - start_rating
|
|
577
|
+
|
|
578
|
+
q = df["pw"].quantile(0.99, "linear")
|
|
579
|
+
expected_ratio = min(1.0, 10.0 / q) / min(1.0, 20.0 / q)
|
|
580
|
+
|
|
581
|
+
assert gen.scale_participation_weights is True
|
|
582
|
+
assert p1_change / p2_change == pytest.approx(expected_ratio, rel=1e-6)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def test_fit_transform_auto_scale_logs_warning_when_out_of_bounds(base_cn, caplog):
|
|
586
|
+
"""Auto-scaling should emit a warning when participation weights exceed [0, 1]."""
|
|
587
|
+
df = pl.DataFrame(
|
|
588
|
+
{
|
|
589
|
+
"pid": ["P1", "P2", "O1", "O2"],
|
|
590
|
+
"tid": ["T1", "T1", "T2", "T2"],
|
|
591
|
+
"mid": ["M1", "M1", "M1", "M1"],
|
|
592
|
+
"dt": ["2024-01-01"] * 4,
|
|
593
|
+
"perf": [0.9, 0.9, 0.1, 0.1],
|
|
594
|
+
"pw": [10.0, 20.0, 10.0, 10.0],
|
|
595
|
+
}
|
|
596
|
+
)
|
|
597
|
+
gen = PlayerRatingGenerator(
|
|
598
|
+
performance_column="perf",
|
|
599
|
+
column_names=base_cn,
|
|
600
|
+
auto_scale_performance=True,
|
|
601
|
+
start_harcoded_start_rating=1000.0,
|
|
602
|
+
)
|
|
603
|
+
with caplog.at_level("WARNING"):
|
|
604
|
+
gen.fit_transform(df)
|
|
605
|
+
|
|
606
|
+
assert any(
|
|
607
|
+
"Auto-scaling participation weights" in record.message for record in caplog.records
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
|
|
554
611
|
def test_future_transform_scales_projected_participation_weight_by_fit_quantile():
|
|
555
612
|
"""Future projected participation weights should scale with fit quantile and be clipped."""
|
|
556
613
|
cn = ColumnNames(
|
|
@@ -1689,3 +1746,73 @@ def test_fit_transform__player_rating_difference_from_team_projected_feature(bas
|
|
|
1689
1746
|
for row in result.iter_rows(named=True):
|
|
1690
1747
|
expected = row[player_col] - row[team_col]
|
|
1691
1748
|
assert row[diff_col] == pytest.approx(expected, rel=1e-9)
|
|
1749
|
+
|
|
1750
|
+
|
|
1751
|
+
def test_fit_transform__start_league_quantile_uses_existing_player_ratings(base_cn):
|
|
1752
|
+
"""
|
|
1753
|
+
Bug reproduction: start_league_quantile should use percentile of existing player
|
|
1754
|
+
ratings for new players, but update_players_to_leagues is never called so
|
|
1755
|
+
_league_player_ratings stays empty and all new players get default rating.
|
|
1756
|
+
|
|
1757
|
+
Expected: New player P_NEW should start at 5th percentile of existing ratings (~920)
|
|
1758
|
+
Actual: New player starts at default 1000 because _league_player_ratings is empty
|
|
1759
|
+
"""
|
|
1760
|
+
import numpy as np
|
|
1761
|
+
|
|
1762
|
+
num_existing_players = 60
|
|
1763
|
+
player_ids = [f"P{i}" for i in range(num_existing_players)]
|
|
1764
|
+
team_ids = [f"T{i % 2 + 1}" for i in range(num_existing_players)]
|
|
1765
|
+
|
|
1766
|
+
df1 = pl.DataFrame(
|
|
1767
|
+
{
|
|
1768
|
+
"pid": player_ids,
|
|
1769
|
+
"tid": team_ids,
|
|
1770
|
+
"mid": ["M1"] * num_existing_players,
|
|
1771
|
+
"dt": ["2024-01-01"] * num_existing_players,
|
|
1772
|
+
"perf": [0.3 + (i % 10) * 0.07 for i in range(num_existing_players)],
|
|
1773
|
+
"pw": [1.0] * num_existing_players,
|
|
1774
|
+
}
|
|
1775
|
+
)
|
|
1776
|
+
|
|
1777
|
+
gen = PlayerRatingGenerator(
|
|
1778
|
+
performance_column="perf",
|
|
1779
|
+
column_names=base_cn,
|
|
1780
|
+
auto_scale_performance=True,
|
|
1781
|
+
start_league_quantile=0.05,
|
|
1782
|
+
start_min_count_for_percentiles=50,
|
|
1783
|
+
features_out=[RatingKnownFeatures.PLAYER_OFF_RATING],
|
|
1784
|
+
)
|
|
1785
|
+
gen.fit_transform(df1)
|
|
1786
|
+
|
|
1787
|
+
existing_ratings = [
|
|
1788
|
+
gen._player_off_ratings[pid].rating_value for pid in player_ids
|
|
1789
|
+
]
|
|
1790
|
+
expected_quantile_rating = np.percentile(existing_ratings, 5)
|
|
1791
|
+
|
|
1792
|
+
srg = gen.start_rating_generator
|
|
1793
|
+
assert len(srg._league_player_ratings.get(None, [])) >= 50, (
|
|
1794
|
+
f"Expected _league_player_ratings to have >=50 entries but got "
|
|
1795
|
+
f"{len(srg._league_player_ratings.get(None, []))}. "
|
|
1796
|
+
"update_players_to_leagues is never called."
|
|
1797
|
+
)
|
|
1798
|
+
|
|
1799
|
+
df2 = pl.DataFrame(
|
|
1800
|
+
{
|
|
1801
|
+
"pid": ["P_NEW", "P0"],
|
|
1802
|
+
"tid": ["T1", "T2"],
|
|
1803
|
+
"mid": ["M2", "M2"],
|
|
1804
|
+
"dt": ["2024-01-02", "2024-01-02"],
|
|
1805
|
+
"pw": [1.0, 1.0],
|
|
1806
|
+
}
|
|
1807
|
+
)
|
|
1808
|
+
result = gen.future_transform(df2)
|
|
1809
|
+
|
|
1810
|
+
new_player_start_rating = result.filter(pl.col("pid") == "P_NEW")[
|
|
1811
|
+
"player_off_rating_perf"
|
|
1812
|
+
][0]
|
|
1813
|
+
|
|
1814
|
+
assert new_player_start_rating == pytest.approx(expected_quantile_rating, rel=0.1), (
|
|
1815
|
+
f"New player should start at 5th percentile ({expected_quantile_rating:.1f}) "
|
|
1816
|
+
f"but got {new_player_start_rating:.1f}. "
|
|
1817
|
+
"start_league_quantile has no effect because update_players_to_leagues is never called."
|
|
1818
|
+
)
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Tests to ensure PlayerRatingGenerator does not mutate input columns."""
|
|
2
|
+
|
|
3
|
+
import polars as pl
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from spforge import ColumnNames
|
|
7
|
+
from spforge.ratings import PlayerRatingGenerator, RatingKnownFeatures
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def cn_with_projected():
|
|
12
|
+
"""ColumnNames with both participation_weight and projected_participation_weight."""
|
|
13
|
+
return ColumnNames(
|
|
14
|
+
player_id="pid",
|
|
15
|
+
team_id="tid",
|
|
16
|
+
match_id="mid",
|
|
17
|
+
start_date="dt",
|
|
18
|
+
update_match_id="mid",
|
|
19
|
+
participation_weight="minutes",
|
|
20
|
+
projected_participation_weight="minutes_prediction",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture
|
|
25
|
+
def fit_df():
|
|
26
|
+
"""Training data with minutes > 1 (will trigger auto-scaling)."""
|
|
27
|
+
return pl.DataFrame(
|
|
28
|
+
{
|
|
29
|
+
"pid": ["P1", "P2", "P3", "P4"],
|
|
30
|
+
"tid": ["T1", "T1", "T2", "T2"],
|
|
31
|
+
"mid": ["M1", "M1", "M1", "M1"],
|
|
32
|
+
"dt": ["2024-01-01"] * 4,
|
|
33
|
+
"perf": [0.6, 0.4, 0.7, 0.3],
|
|
34
|
+
"minutes": [30.0, 25.0, 32.0, 28.0],
|
|
35
|
+
"minutes_prediction": [28.0, 24.0, 30.0, 26.0],
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.fixture
|
|
41
|
+
def future_df():
|
|
42
|
+
"""Future prediction data with minutes > 1 (will trigger auto-scaling)."""
|
|
43
|
+
return pl.DataFrame(
|
|
44
|
+
{
|
|
45
|
+
"pid": ["P1", "P2", "P3", "P4"],
|
|
46
|
+
"tid": ["T1", "T1", "T2", "T2"],
|
|
47
|
+
"mid": ["M2", "M2", "M2", "M2"],
|
|
48
|
+
"dt": ["2024-01-02"] * 4,
|
|
49
|
+
"minutes": [30.0, 25.0, 32.0, 28.0],
|
|
50
|
+
"minutes_prediction": [28.0, 24.0, 30.0, 26.0],
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_fit_transform_does_not_mutate_participation_weight(cn_with_projected, fit_df):
|
|
56
|
+
"""fit_transform should not modify the participation_weight column values."""
|
|
57
|
+
# Join result with original to compare values by player_id
|
|
58
|
+
gen = PlayerRatingGenerator(
|
|
59
|
+
performance_column="perf",
|
|
60
|
+
column_names=cn_with_projected,
|
|
61
|
+
auto_scale_performance=True,
|
|
62
|
+
features_out=[RatingKnownFeatures.PLAYER_OFF_RATING],
|
|
63
|
+
)
|
|
64
|
+
result = gen.fit_transform(fit_df)
|
|
65
|
+
|
|
66
|
+
# Check that each player's minutes value is preserved
|
|
67
|
+
original_by_player = dict(zip(fit_df["pid"].to_list(), fit_df["minutes"].to_list()))
|
|
68
|
+
result_by_player = dict(zip(result["pid"].to_list(), result["minutes"].to_list()))
|
|
69
|
+
|
|
70
|
+
for pid, original_val in original_by_player.items():
|
|
71
|
+
result_val = result_by_player[pid]
|
|
72
|
+
assert result_val == original_val, (
|
|
73
|
+
f"participation_weight for player {pid} was mutated. "
|
|
74
|
+
f"Expected {original_val}, got {result_val}"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_fit_transform_does_not_mutate_projected_participation_weight(cn_with_projected, fit_df):
|
|
79
|
+
"""fit_transform should not modify the projected_participation_weight column values."""
|
|
80
|
+
gen = PlayerRatingGenerator(
|
|
81
|
+
performance_column="perf",
|
|
82
|
+
column_names=cn_with_projected,
|
|
83
|
+
auto_scale_performance=True,
|
|
84
|
+
features_out=[RatingKnownFeatures.PLAYER_OFF_RATING],
|
|
85
|
+
)
|
|
86
|
+
result = gen.fit_transform(fit_df)
|
|
87
|
+
|
|
88
|
+
# Check that each player's minutes_prediction value is preserved
|
|
89
|
+
original_by_player = dict(zip(fit_df["pid"].to_list(), fit_df["minutes_prediction"].to_list()))
|
|
90
|
+
result_by_player = dict(zip(result["pid"].to_list(), result["minutes_prediction"].to_list()))
|
|
91
|
+
|
|
92
|
+
for pid, original_val in original_by_player.items():
|
|
93
|
+
result_val = result_by_player[pid]
|
|
94
|
+
assert result_val == original_val, (
|
|
95
|
+
f"projected_participation_weight for player {pid} was mutated. "
|
|
96
|
+
f"Expected {original_val}, got {result_val}"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_transform_does_not_mutate_participation_weight(cn_with_projected, fit_df, future_df):
|
|
101
|
+
"""transform should not modify the participation_weight column values."""
|
|
102
|
+
gen = PlayerRatingGenerator(
|
|
103
|
+
performance_column="perf",
|
|
104
|
+
column_names=cn_with_projected,
|
|
105
|
+
auto_scale_performance=True,
|
|
106
|
+
features_out=[RatingKnownFeatures.PLAYER_OFF_RATING],
|
|
107
|
+
)
|
|
108
|
+
gen.fit_transform(fit_df)
|
|
109
|
+
|
|
110
|
+
result = gen.transform(future_df)
|
|
111
|
+
|
|
112
|
+
# Check that each player's minutes value is preserved
|
|
113
|
+
original_by_player = dict(zip(future_df["pid"].to_list(), future_df["minutes"].to_list()))
|
|
114
|
+
result_by_player = dict(zip(result["pid"].to_list(), result["minutes"].to_list()))
|
|
115
|
+
|
|
116
|
+
for pid, original_val in original_by_player.items():
|
|
117
|
+
result_val = result_by_player[pid]
|
|
118
|
+
assert result_val == original_val, (
|
|
119
|
+
f"participation_weight for player {pid} was mutated during transform. "
|
|
120
|
+
f"Expected {original_val}, got {result_val}"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_transform_does_not_mutate_projected_participation_weight(cn_with_projected, fit_df, future_df):
|
|
125
|
+
"""transform should not modify the projected_participation_weight column values."""
|
|
126
|
+
gen = PlayerRatingGenerator(
|
|
127
|
+
performance_column="perf",
|
|
128
|
+
column_names=cn_with_projected,
|
|
129
|
+
auto_scale_performance=True,
|
|
130
|
+
features_out=[RatingKnownFeatures.PLAYER_OFF_RATING],
|
|
131
|
+
)
|
|
132
|
+
gen.fit_transform(fit_df)
|
|
133
|
+
|
|
134
|
+
result = gen.transform(future_df)
|
|
135
|
+
|
|
136
|
+
# Check that each player's minutes_prediction value is preserved
|
|
137
|
+
original_by_player = dict(zip(future_df["pid"].to_list(), future_df["minutes_prediction"].to_list()))
|
|
138
|
+
result_by_player = dict(zip(result["pid"].to_list(), result["minutes_prediction"].to_list()))
|
|
139
|
+
|
|
140
|
+
for pid, original_val in original_by_player.items():
|
|
141
|
+
result_val = result_by_player[pid]
|
|
142
|
+
assert result_val == original_val, (
|
|
143
|
+
f"projected_participation_weight for player {pid} was mutated during transform. "
|
|
144
|
+
f"Expected {original_val}, got {result_val}"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_future_transform_does_not_mutate_participation_weight(cn_with_projected, fit_df, future_df):
|
|
149
|
+
"""future_transform should not modify the participation_weight column values."""
|
|
150
|
+
gen = PlayerRatingGenerator(
|
|
151
|
+
performance_column="perf",
|
|
152
|
+
column_names=cn_with_projected,
|
|
153
|
+
auto_scale_performance=True,
|
|
154
|
+
features_out=[RatingKnownFeatures.PLAYER_OFF_RATING],
|
|
155
|
+
)
|
|
156
|
+
gen.fit_transform(fit_df)
|
|
157
|
+
|
|
158
|
+
original_minutes = future_df["minutes"].to_list()
|
|
159
|
+
result = gen.future_transform(future_df)
|
|
160
|
+
|
|
161
|
+
# The minutes column should have the same values as before
|
|
162
|
+
result_minutes = result["minutes"].to_list()
|
|
163
|
+
assert result_minutes == original_minutes, (
|
|
164
|
+
f"participation_weight column was mutated during future_transform. "
|
|
165
|
+
f"Expected {original_minutes}, got {result_minutes}"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_future_transform_does_not_mutate_projected_participation_weight(cn_with_projected, fit_df, future_df):
|
|
170
|
+
"""future_transform should not modify the projected_participation_weight column values."""
|
|
171
|
+
gen = PlayerRatingGenerator(
|
|
172
|
+
performance_column="perf",
|
|
173
|
+
column_names=cn_with_projected,
|
|
174
|
+
auto_scale_performance=True,
|
|
175
|
+
features_out=[RatingKnownFeatures.PLAYER_OFF_RATING],
|
|
176
|
+
)
|
|
177
|
+
gen.fit_transform(fit_df)
|
|
178
|
+
|
|
179
|
+
original_minutes_pred = future_df["minutes_prediction"].to_list()
|
|
180
|
+
result = gen.future_transform(future_df)
|
|
181
|
+
|
|
182
|
+
# The minutes_prediction column should have the same values as before
|
|
183
|
+
result_minutes_pred = result["minutes_prediction"].to_list()
|
|
184
|
+
assert result_minutes_pred == original_minutes_pred, (
|
|
185
|
+
f"projected_participation_weight column was mutated during future_transform. "
|
|
186
|
+
f"Expected {original_minutes_pred}, got {result_minutes_pred}"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_multiple_transforms_do_not_compound_scaling(cn_with_projected, fit_df, future_df):
|
|
191
|
+
"""Multiple transform calls should not compound the scaling effect."""
|
|
192
|
+
gen = PlayerRatingGenerator(
|
|
193
|
+
performance_column="perf",
|
|
194
|
+
column_names=cn_with_projected,
|
|
195
|
+
auto_scale_performance=True,
|
|
196
|
+
features_out=[RatingKnownFeatures.PLAYER_OFF_RATING],
|
|
197
|
+
)
|
|
198
|
+
gen.fit_transform(fit_df)
|
|
199
|
+
|
|
200
|
+
# Call transform multiple times
|
|
201
|
+
result1 = gen.transform(future_df)
|
|
202
|
+
result2 = gen.transform(result1)
|
|
203
|
+
result3 = gen.transform(result2)
|
|
204
|
+
|
|
205
|
+
# After 3 transforms, each player's values should still be the same as original
|
|
206
|
+
original_by_player = dict(zip(future_df["pid"].to_list(), future_df["minutes_prediction"].to_list()))
|
|
207
|
+
final_by_player = dict(zip(result3["pid"].to_list(), result3["minutes_prediction"].to_list()))
|
|
208
|
+
|
|
209
|
+
for pid, original_val in original_by_player.items():
|
|
210
|
+
final_val = final_by_player[pid]
|
|
211
|
+
assert final_val == original_val, (
|
|
212
|
+
f"Multiple transforms compounded the scaling for player {pid}. "
|
|
213
|
+
f"Expected {original_val}, got {final_val}"
|
|
214
|
+
)
|