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