spforge 0.8.14__tar.gz → 0.8.16__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.

Files changed (119) hide show
  1. {spforge-0.8.14/spforge.egg-info → spforge-0.8.16}/PKG-INFO +1 -1
  2. {spforge-0.8.14 → spforge-0.8.16}/pyproject.toml +1 -1
  3. {spforge-0.8.14 → spforge-0.8.16}/spforge/autopipeline.py +20 -4
  4. {spforge-0.8.14 → spforge-0.8.16}/spforge/ratings/_player_rating.py +30 -8
  5. {spforge-0.8.14 → spforge-0.8.16}/spforge/ratings/start_rating_generator.py +1 -1
  6. {spforge-0.8.14 → spforge-0.8.16}/spforge/ratings/team_start_rating_generator.py +1 -1
  7. {spforge-0.8.14 → spforge-0.8.16/spforge.egg-info}/PKG-INFO +1 -1
  8. {spforge-0.8.14 → spforge-0.8.16}/tests/ratings/test_player_rating_generator.py +70 -0
  9. {spforge-0.8.14 → spforge-0.8.16}/tests/test_autopipeline.py +22 -0
  10. {spforge-0.8.14 → spforge-0.8.16}/LICENSE +0 -0
  11. {spforge-0.8.14 → spforge-0.8.16}/MANIFEST.in +0 -0
  12. {spforge-0.8.14 → spforge-0.8.16}/README.md +0 -0
  13. {spforge-0.8.14 → spforge-0.8.16}/examples/__init__.py +0 -0
  14. {spforge-0.8.14 → spforge-0.8.16}/examples/game_level_example.py +0 -0
  15. {spforge-0.8.14 → spforge-0.8.16}/examples/lol/__init__.py +0 -0
  16. {spforge-0.8.14 → spforge-0.8.16}/examples/lol/data/__init__.py +0 -0
  17. {spforge-0.8.14 → spforge-0.8.16}/examples/lol/data/subsample_lol_data.parquet +0 -0
  18. {spforge-0.8.14 → spforge-0.8.16}/examples/lol/data/utils.py +0 -0
  19. {spforge-0.8.14 → spforge-0.8.16}/examples/lol/pipeline_transformer_example.py +0 -0
  20. {spforge-0.8.14 → spforge-0.8.16}/examples/nba/__init__.py +0 -0
  21. {spforge-0.8.14 → spforge-0.8.16}/examples/nba/cross_validation_example.py +0 -0
  22. {spforge-0.8.14 → spforge-0.8.16}/examples/nba/data/__init__.py +0 -0
  23. {spforge-0.8.14 → spforge-0.8.16}/examples/nba/data/game_player_subsample.parquet +0 -0
  24. {spforge-0.8.14 → spforge-0.8.16}/examples/nba/data/utils.py +0 -0
  25. {spforge-0.8.14 → spforge-0.8.16}/examples/nba/feature_engineering_example.py +0 -0
  26. {spforge-0.8.14 → spforge-0.8.16}/examples/nba/game_winner_example.py +0 -0
  27. {spforge-0.8.14 → spforge-0.8.16}/examples/nba/predictor_transformers_example.py +0 -0
  28. {spforge-0.8.14 → spforge-0.8.16}/setup.cfg +0 -0
  29. {spforge-0.8.14 → spforge-0.8.16}/spforge/__init__.py +0 -0
  30. {spforge-0.8.14 → spforge-0.8.16}/spforge/base_feature_generator.py +0 -0
  31. {spforge-0.8.14 → spforge-0.8.16}/spforge/cross_validator/__init__.py +0 -0
  32. {spforge-0.8.14 → spforge-0.8.16}/spforge/cross_validator/_base.py +0 -0
  33. {spforge-0.8.14 → spforge-0.8.16}/spforge/cross_validator/cross_validator.py +0 -0
  34. {spforge-0.8.14 → spforge-0.8.16}/spforge/data_structures.py +0 -0
  35. {spforge-0.8.14 → spforge-0.8.16}/spforge/distributions/__init__.py +0 -0
  36. {spforge-0.8.14 → spforge-0.8.16}/spforge/distributions/_negative_binomial_estimator.py +0 -0
  37. {spforge-0.8.14 → spforge-0.8.16}/spforge/distributions/_normal_distribution_predictor.py +0 -0
  38. {spforge-0.8.14 → spforge-0.8.16}/spforge/distributions/_student_t_distribution_estimator.py +0 -0
  39. {spforge-0.8.14 → spforge-0.8.16}/spforge/estimator/__init__.py +0 -0
  40. {spforge-0.8.14 → spforge-0.8.16}/spforge/estimator/_conditional_estimator.py +0 -0
  41. {spforge-0.8.14 → spforge-0.8.16}/spforge/estimator/_frequency_bucketing_classifier.py +0 -0
  42. {spforge-0.8.14 → spforge-0.8.16}/spforge/estimator/_granularity_estimator.py +0 -0
  43. {spforge-0.8.14 → spforge-0.8.16}/spforge/estimator/_group_by_estimator.py +0 -0
  44. {spforge-0.8.14 → spforge-0.8.16}/spforge/estimator/_ordinal_classifier.py +0 -0
  45. {spforge-0.8.14 → spforge-0.8.16}/spforge/estimator/_sklearn_enhancer_estimator.py +0 -0
  46. {spforge-0.8.14 → spforge-0.8.16}/spforge/feature_generator/__init__.py +0 -0
  47. {spforge-0.8.14 → spforge-0.8.16}/spforge/feature_generator/_base.py +0 -0
  48. {spforge-0.8.14 → spforge-0.8.16}/spforge/feature_generator/_lag.py +0 -0
  49. {spforge-0.8.14 → spforge-0.8.16}/spforge/feature_generator/_net_over_predicted.py +0 -0
  50. {spforge-0.8.14 → spforge-0.8.16}/spforge/feature_generator/_regressor_feature_generator.py +0 -0
  51. {spforge-0.8.14 → spforge-0.8.16}/spforge/feature_generator/_rolling_against_opponent.py +0 -0
  52. {spforge-0.8.14 → spforge-0.8.16}/spforge/feature_generator/_rolling_mean_binary.py +0 -0
  53. {spforge-0.8.14 → spforge-0.8.16}/spforge/feature_generator/_rolling_mean_days.py +0 -0
  54. {spforge-0.8.14 → spforge-0.8.16}/spforge/feature_generator/_rolling_window.py +0 -0
  55. {spforge-0.8.14 → spforge-0.8.16}/spforge/feature_generator/_utils.py +0 -0
  56. {spforge-0.8.14 → spforge-0.8.16}/spforge/features_generator_pipeline.py +0 -0
  57. {spforge-0.8.14 → spforge-0.8.16}/spforge/hyperparameter_tuning/__init__.py +0 -0
  58. {spforge-0.8.14 → spforge-0.8.16}/spforge/hyperparameter_tuning/_default_search_spaces.py +0 -0
  59. {spforge-0.8.14 → spforge-0.8.16}/spforge/hyperparameter_tuning/_tuner.py +0 -0
  60. {spforge-0.8.14 → spforge-0.8.16}/spforge/performance_transformers/__init__.py +0 -0
  61. {spforge-0.8.14 → spforge-0.8.16}/spforge/performance_transformers/_performance_manager.py +0 -0
  62. {spforge-0.8.14 → spforge-0.8.16}/spforge/performance_transformers/_performances_transformers.py +0 -0
  63. {spforge-0.8.14 → spforge-0.8.16}/spforge/ratings/__init__.py +0 -0
  64. {spforge-0.8.14 → spforge-0.8.16}/spforge/ratings/_base.py +0 -0
  65. {spforge-0.8.14 → spforge-0.8.16}/spforge/ratings/_team_rating.py +0 -0
  66. {spforge-0.8.14 → spforge-0.8.16}/spforge/ratings/enums.py +0 -0
  67. {spforge-0.8.14 → spforge-0.8.16}/spforge/ratings/league_identifier.py +0 -0
  68. {spforge-0.8.14 → spforge-0.8.16}/spforge/ratings/league_start_rating_optimizer.py +0 -0
  69. {spforge-0.8.14 → spforge-0.8.16}/spforge/ratings/player_performance_predictor.py +0 -0
  70. {spforge-0.8.14 → spforge-0.8.16}/spforge/ratings/team_performance_predictor.py +0 -0
  71. {spforge-0.8.14 → spforge-0.8.16}/spforge/ratings/utils.py +0 -0
  72. {spforge-0.8.14 → spforge-0.8.16}/spforge/scorer/__init__.py +0 -0
  73. {spforge-0.8.14 → spforge-0.8.16}/spforge/scorer/_score.py +0 -0
  74. {spforge-0.8.14 → spforge-0.8.16}/spforge/transformers/__init__.py +0 -0
  75. {spforge-0.8.14 → spforge-0.8.16}/spforge/transformers/_base.py +0 -0
  76. {spforge-0.8.14 → spforge-0.8.16}/spforge/transformers/_net_over_predicted.py +0 -0
  77. {spforge-0.8.14 → spforge-0.8.16}/spforge/transformers/_operator.py +0 -0
  78. {spforge-0.8.14 → spforge-0.8.16}/spforge/transformers/_other_transformer.py +0 -0
  79. {spforge-0.8.14 → spforge-0.8.16}/spforge/transformers/_predictor.py +0 -0
  80. {spforge-0.8.14 → spforge-0.8.16}/spforge/transformers/_simple_transformer.py +0 -0
  81. {spforge-0.8.14 → spforge-0.8.16}/spforge/transformers/_team_ratio_predictor.py +0 -0
  82. {spforge-0.8.14 → spforge-0.8.16}/spforge/utils.py +0 -0
  83. {spforge-0.8.14 → spforge-0.8.16}/spforge.egg-info/SOURCES.txt +0 -0
  84. {spforge-0.8.14 → spforge-0.8.16}/spforge.egg-info/dependency_links.txt +0 -0
  85. {spforge-0.8.14 → spforge-0.8.16}/spforge.egg-info/requires.txt +0 -0
  86. {spforge-0.8.14 → spforge-0.8.16}/spforge.egg-info/top_level.txt +0 -0
  87. {spforge-0.8.14 → spforge-0.8.16}/tests/cross_validator/test_cross_validator.py +0 -0
  88. {spforge-0.8.14 → spforge-0.8.16}/tests/distributions/test_distribution.py +0 -0
  89. {spforge-0.8.14 → spforge-0.8.16}/tests/end_to_end/test_estimator_hyperparameter_tuning.py +0 -0
  90. {spforge-0.8.14 → spforge-0.8.16}/tests/end_to_end/test_league_start_rating_optimizer.py +0 -0
  91. {spforge-0.8.14 → spforge-0.8.16}/tests/end_to_end/test_lol_player_kills.py +0 -0
  92. {spforge-0.8.14 → spforge-0.8.16}/tests/end_to_end/test_nba_player_points.py +0 -0
  93. {spforge-0.8.14 → spforge-0.8.16}/tests/end_to_end/test_nba_player_ratings_hyperparameter_tuning.py +0 -0
  94. {spforge-0.8.14 → spforge-0.8.16}/tests/end_to_end/test_nba_prediction_consistency.py +0 -0
  95. {spforge-0.8.14 → spforge-0.8.16}/tests/estimator/test_sklearn_estimator.py +0 -0
  96. {spforge-0.8.14 → spforge-0.8.16}/tests/feature_generator/test_lag.py +0 -0
  97. {spforge-0.8.14 → spforge-0.8.16}/tests/feature_generator/test_regressor_feature_generator.py +0 -0
  98. {spforge-0.8.14 → spforge-0.8.16}/tests/feature_generator/test_rolling_against_opponent.py +0 -0
  99. {spforge-0.8.14 → spforge-0.8.16}/tests/feature_generator/test_rolling_mean_binary.py +0 -0
  100. {spforge-0.8.14 → spforge-0.8.16}/tests/feature_generator/test_rolling_mean_days.py +0 -0
  101. {spforge-0.8.14 → spforge-0.8.16}/tests/feature_generator/test_rolling_window.py +0 -0
  102. {spforge-0.8.14 → spforge-0.8.16}/tests/hyperparameter_tuning/test_estimator_tuner.py +0 -0
  103. {spforge-0.8.14 → spforge-0.8.16}/tests/hyperparameter_tuning/test_rating_tuner.py +0 -0
  104. {spforge-0.8.14 → spforge-0.8.16}/tests/performance_transformers/test_performance_manager.py +0 -0
  105. {spforge-0.8.14 → spforge-0.8.16}/tests/performance_transformers/test_performances_transformers.py +0 -0
  106. {spforge-0.8.14 → spforge-0.8.16}/tests/ratings/test_player_rating_no_mutation.py +0 -0
  107. {spforge-0.8.14 → spforge-0.8.16}/tests/ratings/test_ratings_property.py +0 -0
  108. {spforge-0.8.14 → spforge-0.8.16}/tests/ratings/test_team_rating_generator.py +0 -0
  109. {spforge-0.8.14 → spforge-0.8.16}/tests/ratings/test_utils_scaled_weights.py +0 -0
  110. {spforge-0.8.14 → spforge-0.8.16}/tests/scorer/test_score.py +0 -0
  111. {spforge-0.8.14 → spforge-0.8.16}/tests/scorer/test_score_aggregation_granularity.py +0 -0
  112. {spforge-0.8.14 → spforge-0.8.16}/tests/test_autopipeline_context.py +0 -0
  113. {spforge-0.8.14 → spforge-0.8.16}/tests/test_feature_generator_pipeline.py +0 -0
  114. {spforge-0.8.14 → spforge-0.8.16}/tests/transformers/test_estimator_transformer_context.py +0 -0
  115. {spforge-0.8.14 → spforge-0.8.16}/tests/transformers/test_net_over_predicted.py +0 -0
  116. {spforge-0.8.14 → spforge-0.8.16}/tests/transformers/test_other_transformer.py +0 -0
  117. {spforge-0.8.14 → spforge-0.8.16}/tests/transformers/test_predictor_transformer.py +0 -0
  118. {spforge-0.8.14 → spforge-0.8.16}/tests/transformers/test_simple_transformer.py +0 -0
  119. {spforge-0.8.14 → spforge-0.8.16}/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.14
3
+ Version: 0.8.16
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.14"
7
+ version = "0.8.16"
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"
@@ -264,6 +264,7 @@ class AutoPipeline(BaseEstimator):
264
264
  self.numeric_features = numeric_features
265
265
  self.remainder = remainder
266
266
  self._cat_feats = []
267
+ self._filter_feature_names: list[str] = []
267
268
 
268
269
  # Auto-compute context features
269
270
  self.context_feature_names = self._compute_context_features()
@@ -276,11 +277,12 @@ class AutoPipeline(BaseEstimator):
276
277
  self._resolved_categorical_handling: CategoricalHandling | None = None
277
278
 
278
279
  def _compute_context_features(self) -> list[str]:
279
- """Auto-compute context features from estimator, granularity, and filters.
280
+ """Auto-compute context features from estimator and granularity.
280
281
 
281
282
  Note: Context from predictor_transformers is tracked separately in
282
283
  context_predictor_transformer_feature_names and is dropped before
283
- the final estimator.
284
+ the final estimator. Filter columns are tracked separately and are
285
+ dropped before the final estimator.
284
286
  """
285
287
  from spforge.transformers._base import PredictorTransformer
286
288
 
@@ -325,8 +327,10 @@ class AutoPipeline(BaseEstimator):
325
327
  context.extend(self.granularity)
326
328
 
327
329
  # Add filter columns
330
+ self._filter_feature_names = []
328
331
  for f in self.filters:
329
- context.append(f.column_name)
332
+ if f.column_name not in self._filter_feature_names:
333
+ self._filter_feature_names.append(f.column_name)
330
334
 
331
335
  # Dedupe while preserving order, excluding estimator_features
332
336
  seen = set()
@@ -540,8 +544,10 @@ class AutoPipeline(BaseEstimator):
540
544
  prev_transformer_feats_out.extend(feats_out)
541
545
 
542
546
  # Use FunctionTransformer with global function for serializability
547
+ drop_filter_cols = set(self._filter_feature_names)
548
+ drop_cols = drop_ctx_set | drop_filter_cols
543
549
  final = FunctionTransformer(
544
- _drop_columns_transformer, validate=False, kw_args={"drop_cols": drop_ctx_set}
550
+ _drop_columns_transformer, validate=False, kw_args={"drop_cols": drop_cols}
545
551
  )
546
552
  steps.append(("final", final))
547
553
 
@@ -572,6 +578,7 @@ class AutoPipeline(BaseEstimator):
572
578
  self.feature_names
573
579
  + self.context_feature_names
574
580
  + self.context_predictor_transformer_feature_names
581
+ + self._filter_feature_names
575
582
  + self.granularity
576
583
  )
577
584
  )
@@ -660,6 +667,11 @@ class AutoPipeline(BaseEstimator):
660
667
  if ctx not in all_features:
661
668
  all_features.append(ctx)
662
669
 
670
+ # Add filter columns (needed for fit-time filtering)
671
+ for col in self._filter_feature_names:
672
+ if col not in all_features:
673
+ all_features.append(col)
674
+
663
675
  return all_features
664
676
 
665
677
  def _get_estimator_feature_names(self) -> list[str]:
@@ -679,6 +691,10 @@ class AutoPipeline(BaseEstimator):
679
691
  context_set = set(self.context_feature_names)
680
692
  features = [f for f in features if f not in context_set]
681
693
 
694
+ # Remove filter columns (used only for fit-time filtering)
695
+ filter_set = set(self._filter_feature_names)
696
+ features = [f for f in features if f not in filter_set]
697
+
682
698
  return features
683
699
 
684
700
  def _resolve_importance_feature_names(self, estimator, n_features: int) -> list[str]:
@@ -16,6 +16,7 @@ from spforge.data_structures import (
16
16
  MatchPerformance,
17
17
  MatchPlayer,
18
18
  PlayerRating,
19
+ PlayerRatingChange,
19
20
  PlayerRatingsResult,
20
21
  PreMatchPlayerRating,
21
22
  PreMatchPlayersCollection,
@@ -78,7 +79,7 @@ class PlayerRatingGenerator(RatingGenerator):
78
79
  start_min_count_for_percentiles: int = 50,
79
80
  start_team_rating_subtract: float = 80,
80
81
  start_team_weight: float = 0,
81
- start_max_days_ago_league_entities: int = 120,
82
+ start_max_days_ago_league_entities: int = 600,
82
83
  start_min_match_count_team_rating: int = 2,
83
84
  start_harcoded_start_rating: float | None = None,
84
85
  column_names: ColumnNames | None = None,
@@ -442,9 +443,9 @@ class PlayerRatingGenerator(RatingGenerator):
442
443
  team1_off_rating, team1_def_rating = self._team_off_def_rating_from_collection(c1)
443
444
  team2_off_rating, team2_def_rating = self._team_off_def_rating_from_collection(c2)
444
445
 
445
- player_updates: list[tuple[str, str, float, float, float, float, float, float, int]] = (
446
- []
447
- )
446
+ player_updates: list[
447
+ tuple[str, str, float, float, float, float, float, float, int, str | None]
448
+ ] = []
448
449
 
449
450
  for pre_player in c1.pre_match_player_ratings:
450
451
  pid = pre_player.id
@@ -520,6 +521,7 @@ class PlayerRatingGenerator(RatingGenerator):
520
521
  float(off_change),
521
522
  float(def_change),
522
523
  day_number,
524
+ pre_player.league,
523
525
  )
524
526
  )
525
527
 
@@ -597,6 +599,7 @@ class PlayerRatingGenerator(RatingGenerator):
597
599
  float(off_change),
598
600
  float(def_change),
599
601
  day_number,
602
+ pre_player.league,
600
603
  )
601
604
  )
602
605
 
@@ -611,6 +614,7 @@ class PlayerRatingGenerator(RatingGenerator):
611
614
  _off_change,
612
615
  _def_change,
613
616
  _dn,
617
+ _league,
614
618
  ) in player_updates:
615
619
  out[cn.player_id].append(pid)
616
620
  out[cn.match_id].append(match_id)
@@ -627,15 +631,18 @@ class PlayerRatingGenerator(RatingGenerator):
627
631
  for (
628
632
  pid,
629
633
  team_id,
630
- _off_pre,
634
+ off_pre,
631
635
  _def_pre,
632
636
  _pred_off,
633
637
  _pred_def,
634
638
  off_change,
635
639
  def_change,
636
640
  dn,
641
+ league,
637
642
  ) in player_updates:
638
- pending_team_updates.append((pid, team_id, off_change, def_change, dn))
643
+ pending_team_updates.append(
644
+ (pid, team_id, off_pre, off_change, def_change, dn, league)
645
+ )
639
646
 
640
647
  if last_update_id is None:
641
648
  last_update_id = update_id
@@ -645,9 +652,11 @@ class PlayerRatingGenerator(RatingGenerator):
645
652
 
646
653
  return pl.DataFrame(out, strict=False)
647
654
 
648
- def _apply_player_updates(self, updates: list[tuple[str, str, float, float, int]]) -> None:
655
+ def _apply_player_updates(
656
+ self, updates: list[tuple[str, str, float, float, float, int, str | None]]
657
+ ) -> None:
649
658
 
650
- for player_id, team_id, off_change, def_change, day_number in updates:
659
+ for player_id, team_id, pre_rating, off_change, def_change, day_number, league in updates:
651
660
  off_state = self._player_off_ratings[player_id]
652
661
  off_state.confidence_sum = self._calculate_post_match_confidence_sum(
653
662
  entity_rating=off_state,
@@ -670,6 +679,19 @@ class PlayerRatingGenerator(RatingGenerator):
670
679
  def_state.last_match_day_number = int(day_number)
671
680
  def_state.most_recent_team_id = team_id
672
681
 
682
+ self.start_rating_generator.update_players_to_leagues(
683
+ PlayerRatingChange(
684
+ id=player_id,
685
+ day_number=day_number,
686
+ league=league,
687
+ participation_weight=1.0,
688
+ predicted_performance=0.0,
689
+ performance=0.0,
690
+ pre_match_rating_value=pre_rating,
691
+ rating_change_value=off_change,
692
+ )
693
+ )
694
+
673
695
  def _add_rating_features(self, df: pl.DataFrame) -> pl.DataFrame:
674
696
  cols_to_add = set((self._features_out or []) + (self.non_predictor_features_out or []))
675
697
 
@@ -28,7 +28,7 @@ class StartRatingGenerator:
28
28
  min_count_for_percentiles: int = 50,
29
29
  team_rating_subtract: float = 80,
30
30
  team_weight: float = 0,
31
- max_days_ago_league_entities: int = 120,
31
+ max_days_ago_league_entities: int = 600,
32
32
  min_match_count_team_rating: int = 2,
33
33
  harcoded_start_rating: float | None = None,
34
34
  ):
@@ -24,7 +24,7 @@ class TeamStartRatingGenerator:
24
24
  league_ratings: dict[str, float] | None = None,
25
25
  league_quantile: float = 0.2,
26
26
  min_count_for_percentiles: int = 50,
27
- max_days_ago_league_entities: int = 120,
27
+ max_days_ago_league_entities: int = 600,
28
28
  min_match_count_team_rating: int = 2,
29
29
  harcoded_start_rating: float | None = None,
30
30
  ):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spforge
3
- Version: 0.8.14
3
+ Version: 0.8.16
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
@@ -1746,3 +1746,73 @@ def test_fit_transform__player_rating_difference_from_team_projected_feature(bas
1746
1746
  for row in result.iter_rows(named=True):
1747
1747
  expected = row[player_col] - row[team_col]
1748
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
+ )
@@ -12,6 +12,7 @@ from sklearn.linear_model import LinearRegression, LogisticRegression
12
12
 
13
13
  from spforge import AutoPipeline
14
14
  from spforge.estimator import SkLearnEnhancerEstimator
15
+ from spforge.scorer import Filter, Operator
15
16
  from spforge.transformers import EstimatorTransformer
16
17
 
17
18
 
@@ -231,6 +232,27 @@ def test_predict_proba(df_clf):
231
232
  assert np.allclose(proba.sum(axis=1), 1.0, atol=1e-6)
232
233
 
233
234
 
235
+ def test_filter_columns_not_passed_to_estimator(frame):
236
+ df_pd = pd.DataFrame(
237
+ {"x": [1.0, 2.0, 3.0, 4.0], "keep": [1, 0, 1, 0], "y": [1.0, 2.0, 3.0, 4.0]}
238
+ )
239
+ df = df_pd if frame == "pd" else pl.from_pandas(df_pd)
240
+
241
+ model = AutoPipeline(
242
+ estimator=CaptureEstimator(),
243
+ estimator_features=["x"],
244
+ filters=[Filter(column_name="keep", value=1, operator=Operator.EQUALS)],
245
+ )
246
+
247
+ X = _select(df, ["x", "keep"])
248
+ y = _col(df, "y")
249
+ model.fit(X, y=y)
250
+
251
+ est = _inner_estimator(model)
252
+ assert "keep" in model.required_features
253
+ assert "keep" not in est.fit_columns
254
+
255
+
234
256
  def test_predict_proba_raises_if_not_supported(df_reg):
235
257
  model = AutoPipeline(
236
258
  estimator=LinearRegression(),
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes