spforge 0.8.20__py3-none-any.whl → 0.8.25__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of spforge might be problematic. Click here for more details.

@@ -835,6 +835,149 @@ def test_fit_transform_when_performance_out_of_range_then_error_is_raised(column
835
835
  generator.fit_transform(df)
836
836
 
837
837
 
838
+ def test_fit_transform_when_performance_is_null_then_no_rating_change(column_names):
839
+ """
840
+ When performance is null, then we should expect to see no rating change
841
+ because null means missing data, not 0.0 (worst) performance.
842
+ The team's pre-match rating for the next game should equal their rating before the null game.
843
+ """
844
+ generator = TeamRatingGenerator(
845
+ performance_column="won",
846
+ column_names=column_names,
847
+ start_team_rating=1000.0,
848
+ confidence_weight=0.0,
849
+ output_suffix="",
850
+ features_out=[RatingKnownFeatures.TEAM_OFF_RATING_PROJECTED],
851
+ )
852
+
853
+ # Match 1: team_a perf=0.6, team_b perf=0.4
854
+ # Match 2: team_a has null performance, team_b perf=0.6
855
+ # Match 3: team_a perf=0.6, team_b perf=0.4
856
+ df = pl.DataFrame(
857
+ {
858
+ "match_id": [1, 1, 2, 2, 3, 3],
859
+ "team_id": ["team_a", "team_b", "team_a", "team_b", "team_a", "team_b"],
860
+ "start_date": [
861
+ datetime(2024, 1, 1),
862
+ datetime(2024, 1, 1),
863
+ datetime(2024, 1, 2),
864
+ datetime(2024, 1, 2),
865
+ datetime(2024, 1, 3),
866
+ datetime(2024, 1, 3),
867
+ ],
868
+ "won": [0.6, 0.4, None, 0.6, 0.6, 0.4], # team_a has null in match 2
869
+ }
870
+ )
871
+
872
+ result = generator.fit_transform(df)
873
+
874
+ # Get team_a's pre-match rating for match 2 (after match 1) and match 3 (after match 2)
875
+ team_a_rating_before_m2 = result.filter(
876
+ (pl.col("team_id") == "team_a") & (pl.col("match_id") == 2)
877
+ )["team_off_rating_projected"][0]
878
+ team_a_rating_before_m3 = result.filter(
879
+ (pl.col("team_id") == "team_a") & (pl.col("match_id") == 3)
880
+ )["team_off_rating_projected"][0]
881
+
882
+ # Key assertion: rating before M3 should equal rating before M2
883
+ # because null performance in M2 means NO rating change
884
+ assert team_a_rating_before_m3 == team_a_rating_before_m2, (
885
+ f"team_a's rating changed after null performance game! "
886
+ f"Before M2={team_a_rating_before_m2}, Before M3={team_a_rating_before_m3}"
887
+ )
888
+
889
+ # Also verify null is not treated as 0.0 by comparing with explicit 0.0
890
+ # Use 0.3 instead of 0.0 to keep mean in valid range
891
+ df_with_low_perf = df.with_columns(
892
+ pl.when((pl.col("team_id") == "team_a") & (pl.col("match_id") == 2))
893
+ .then(0.3) # Low performance (below predicted ~0.5) causes rating drop
894
+ .otherwise(pl.col("won"))
895
+ .alias("won")
896
+ )
897
+
898
+ gen_low = TeamRatingGenerator(
899
+ performance_column="won",
900
+ column_names=column_names,
901
+ start_team_rating=1000.0,
902
+ confidence_weight=0.0,
903
+ output_suffix="",
904
+ features_out=[RatingKnownFeatures.TEAM_OFF_RATING_PROJECTED],
905
+ )
906
+ result_low = gen_low.fit_transform(df_with_low_perf)
907
+
908
+ team_a_rating_before_m3_with_low = result_low.filter(
909
+ (pl.col("team_id") == "team_a") & (pl.col("match_id") == 3)
910
+ )["team_off_rating_projected"][0]
911
+
912
+ # With low perf (0.3), rating should drop (different from null which has no change)
913
+ assert team_a_rating_before_m3 > team_a_rating_before_m3_with_low, (
914
+ f"Null performance is being treated as low performance! "
915
+ f"Rating with null={team_a_rating_before_m3}, rating with low perf={team_a_rating_before_m3_with_low}"
916
+ )
917
+
918
+
919
+ def test_transform_when_auto_scale_performance_then_uses_correct_column(column_names):
920
+ """
921
+ When auto_scale_performance=True, the performance manager renames the column
922
+ (e.g., 'won' -> 'performance__won'). Transform should still work by applying
923
+ the performance manager to transform the input data.
924
+
925
+ Bug: Currently transform doesn't apply the performance manager, causing
926
+ a column mismatch where it looks for 'performance__won' but data has 'won'.
927
+ This results in None being returned and defaulting to 0.0 performance.
928
+ """
929
+ generator = TeamRatingGenerator(
930
+ performance_column="won",
931
+ column_names=column_names,
932
+ start_team_rating=1000.0,
933
+ confidence_weight=0.0,
934
+ output_suffix="",
935
+ auto_scale_performance=True,
936
+ )
937
+
938
+ # fit_transform with valid performance values
939
+ fit_df = pl.DataFrame(
940
+ {
941
+ "match_id": [1, 1],
942
+ "team_id": ["team_a", "team_b"],
943
+ "start_date": [datetime(2024, 1, 1), datetime(2024, 1, 1)],
944
+ "won": [0.6, 0.4],
945
+ }
946
+ )
947
+ generator.fit_transform(fit_df)
948
+
949
+ # After fit_transform, performance_column is changed to 'performance__won'
950
+ assert generator.performance_column == "performance__won", (
951
+ f"Expected performance_column to be 'performance__won' but got '{generator.performance_column}'"
952
+ )
953
+
954
+ team_a_rating_before = generator._team_off_ratings["team_a"].rating_value
955
+
956
+ # transform with same format data (original column name 'won')
957
+ # team_a has good performance (0.6 > predicted ~0.5), so rating should INCREASE
958
+ transform_df = pl.DataFrame(
959
+ {
960
+ "match_id": [2, 2],
961
+ "team_id": ["team_a", "team_b"],
962
+ "start_date": [datetime(2024, 1, 2), datetime(2024, 1, 2)],
963
+ "won": [0.6, 0.4], # Original column name, not 'performance__won'
964
+ }
965
+ )
966
+
967
+ generator.transform(transform_df)
968
+
969
+ team_a_rating_after = generator._team_off_ratings["team_a"].rating_value
970
+
971
+ # With 0.6 performance (above predicted ~0.5), rating should INCREASE
972
+ # Bug: column mismatch causes perf to default to 0.0, making rating DECREASE
973
+ assert team_a_rating_after > team_a_rating_before, (
974
+ f"Rating should increase with good performance (0.6), but it went from "
975
+ f"{team_a_rating_before} to {team_a_rating_after}. This indicates transform "
976
+ f"is not finding the performance column (looking for '{generator.performance_column}' "
977
+ f"but data has 'won') and defaulting to 0.0 performance."
978
+ )
979
+
980
+
838
981
  @pytest.mark.parametrize("confidence_weight", [0.0, 0.5, 1.0])
839
982
  def test_fit_transform_when_confidence_weight_varies_then_new_teams_have_different_rating_changes(
840
983
  column_names, confidence_weight
@@ -1283,12 +1426,11 @@ def test_transform_when_called_after_fit_transform_then_uses_updated_ratings(
1283
1426
  assert team_a_row["team_off_rating_projected"][0] == pytest.approx(team_a_rating_after_first)
1284
1427
 
1285
1428
 
1286
- def test_transform_when_called_without_performance_column_then_defaults_to_zero(column_names):
1429
+ def test_transform_when_called_without_performance_column_then_no_rating_change(column_names):
1287
1430
  """
1288
1431
  When transform is called without performance column, then we should expect to see
1289
- it works but defaults performance to 0.0 because _calculate_ratings uses
1290
- r.get(self.performance_column) which returns None, defaulting to 0.0.
1291
- This means ratings will be updated as if teams lost (performance=0.0).
1432
+ ratings remain unchanged because null/missing performance means no rating update
1433
+ (not treated as 0.0 which would cause a rating drop).
1292
1434
  """
1293
1435
  generator = TeamRatingGenerator(
1294
1436
  performance_column="won",
@@ -1321,7 +1463,8 @@ def test_transform_when_called_without_performance_column_then_defaults_to_zero(
1321
1463
  generator.transform(df)
1322
1464
  team_a_rating_after = generator._team_off_ratings["team_a"].rating_value
1323
1465
 
1324
- assert team_a_rating_after < team_a_rating_before
1466
+ # Null/missing performance means no rating change
1467
+ assert team_a_rating_after == team_a_rating_before
1325
1468
 
1326
1469
 
1327
1470
  def test_future_transform_when_called_then_ratings_not_updated(basic_rating_generator):
@@ -1539,14 +1682,13 @@ def test_transform_vs_future_transform_when_same_match_then_transform_updates_ra
1539
1682
  assert team_a_rating_after_future == team_a_rating_before_2
1540
1683
 
1541
1684
 
1542
- def test_transform_vs_future_transform_when_performance_column_missing_then_both_work_but_transform_defaults_performance(
1685
+ def test_transform_vs_future_transform_when_performance_column_missing_then_both_work_with_no_rating_change(
1543
1686
  column_names,
1544
1687
  ):
1545
1688
  """
1546
1689
  When performance column is missing, then we should expect to see
1547
- both future_transform and transform work, but transform defaults performance
1548
- to 0.0 (treating it as a loss), while future_transform doesn't need performance
1549
- at all since it only computes predictions.
1690
+ both future_transform and transform work, and both result in no rating change
1691
+ because null/missing performance means no update (not treated as 0.0).
1550
1692
  """
1551
1693
  generator = TeamRatingGenerator(
1552
1694
  performance_column="won",
@@ -1594,8 +1736,9 @@ def test_transform_vs_future_transform_when_performance_column_missing_then_both
1594
1736
  result_transform = generator.transform(transform_df)
1595
1737
  assert result_transform is not None
1596
1738
 
1739
+ # Null/missing performance means no rating change
1597
1740
  team_a_rating_after_transform = generator._team_off_ratings["team_a"].rating_value
1598
- assert team_a_rating_after_transform < team_a_rating_before
1741
+ assert team_a_rating_after_transform == team_a_rating_before
1599
1742
 
1600
1743
 
1601
1744
  def test_transform_vs_future_transform_when_games_played_then_transform_increments_but_future_transform_does_not(
@@ -1878,7 +2021,6 @@ def test_transform_when_date_formats_vary_then_processes_successfully(column_nam
1878
2021
  start_team_rating=1000.0,
1879
2022
  confidence_weight=0.0,
1880
2023
  output_suffix="",
1881
- auto_scale_performance=True,
1882
2024
  )
1883
2025
 
1884
2026
  fit_df = pl.DataFrame(