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.
@@ -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=ZUwv6Q6O8cD0u5TiSqG6lhW0j16RlSb160AzuOeL2R8,23186
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=aXuDvRWvvgK4SEI_DMYscvathmPb6nkMxnqKgG8HC0Y,2769
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=N2sKG4SvG41hlsFT2kx_DQYMmXsQr-8031Tu_rxlxyY,1015
47
- spforge/hyperparameter_tuning/_default_search_spaces.py,sha256=Sm5IrHAW0-vRC8jqCPX0pDi_C-W3L_MoEKGA8bx1Zbc,7546
48
- spforge/hyperparameter_tuning/_tuner.py,sha256=uovhGqhe8-fdhi79aErUmE2h5NCycFQEIRv5WCjpC7E,16732
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=KwAga6dGhNkXi-MDW6LPjwk6VZwCcjo5L--jnk9aio8,9706
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=MyqsyLSY6d7_bxDSnF8eWOyXpSCADWGdepdFSGM4cHw,51365
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=_7hIJ9KRVCwsCoY1GIzY8cuOdHR8RH_BCMeMwQG3E04,6776
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=ZJe84sTvE4Yep3d4wKJMMJn2Q4PhcCwkO7Wyd5nsYUA,5110
63
- spforge/ratings/utils.py,sha256=qms5J5SD-FyXDR2G8giDMbu_AoLgI135pjW4nghxROg,3940
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=TR0T9nJj0aeVgGfOE0fZmXlO66CELulYwxhi7ZAxhvY,56184
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=xLfaFIhkFsigAoitB4x3F8An2j9ymdjQy5VrsTvJlrA,3152
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.8.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
75
- tests/test_autopipeline.py,sha256=WXHeqBdjQD6xaXVkzvS8ocz0WVP9R7lN0PiHJ2iD8nA,16911
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=LXRkI_6Ho2kzJVbNAM17QFhx_MP9WdDJXCO9dWgJGNA,6491
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=PyCFP3KPc4Iy9E_X9stCVxra14uMgC1tuRwuQ30rO_o,13195
95
- tests/performance_transformers/test_performance_manager.py,sha256=bfC5GiBuzHw-mLmKeEzBUUPuKm0ayax2bsF1j88W8L0,10120
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=FGH3Tq0uFoSlkS_XMldsUKhsovBRBvzH9EbqjKvg2O0,59601
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/scorer/test_score.py,sha256=_Vd6tKpy_1GeOxU7Omxci4CFf7PvRGMefEI0gv2gV6A,74688
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.8.dist-info/METADATA,sha256=fO2JHqnnqOrjkWZ1Zh4rgYg58bi4YzxhSa8I72wqDs4,20047
109
- spforge-0.8.8.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
110
- spforge-0.8.8.dist-info/top_level.txt,sha256=6UW2M5a7WKOeaAi900qQmRKNj5-HZzE8-eUD9Y9LTq0,23
111
- spforge-0.8.8.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.10.1)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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
+ )