spforge 0.8.8__py3-none-any.whl → 0.8.18__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/autopipeline.py +169 -5
- spforge/estimator/_group_by_estimator.py +11 -3
- 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.18.dist-info}/METADATA +1 -1
- {spforge-0.8.8.dist-info → spforge-0.8.18.dist-info}/RECORD +20 -18
- {spforge-0.8.8.dist-info → spforge-0.8.18.dist-info}/WHEEL +1 -1
- 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.18.dist-info}/licenses/LICENSE +0 -0
- {spforge-0.8.8.dist-info → spforge-0.8.18.dist-info}/top_level.txt +0 -0
tests/test_autopipeline.py
CHANGED
|
@@ -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(),
|
|
@@ -306,7 +328,18 @@ def test_infer_categorical_from_feature_names_when_only_numeric_features_given(d
|
|
|
306
328
|
assert any(c.startswith("cat") for c in cap.fit_columns)
|
|
307
329
|
|
|
308
330
|
|
|
309
|
-
def test_granularity_groups_rows_before_estimator_fit_and_predict(
|
|
331
|
+
def test_granularity_groups_rows_before_estimator_fit_and_predict(frame):
|
|
332
|
+
df_pd = pd.DataFrame(
|
|
333
|
+
{
|
|
334
|
+
"gameid": ["g1", "g1", "g2", "g2", "g3", "g3"],
|
|
335
|
+
"num1": [1.0, 2.0, np.nan, 4.0, 5.0, 6.0],
|
|
336
|
+
"num2": [10.0, 20.0, 30.0, 40.0, np.nan, 60.0],
|
|
337
|
+
"cat1": ["a", "b", "a", None, "b", "c"],
|
|
338
|
+
"y": [1.0, 1.0, 2.0, 2.0, 3.0, 3.0],
|
|
339
|
+
}
|
|
340
|
+
)
|
|
341
|
+
df = df_pd if frame == "pd" else pl.from_pandas(df_pd)
|
|
342
|
+
|
|
310
343
|
model = AutoPipeline(
|
|
311
344
|
estimator=CaptureEstimator(),
|
|
312
345
|
estimator_features=["gameid", "num1", "num2", "cat1"],
|
|
@@ -317,16 +350,16 @@ def test_granularity_groups_rows_before_estimator_fit_and_predict(df_reg):
|
|
|
317
350
|
remainder="drop",
|
|
318
351
|
)
|
|
319
352
|
|
|
320
|
-
X = _select(
|
|
321
|
-
y = _col(
|
|
353
|
+
X = _select(df, ["gameid", "num1", "num2", "cat1"])
|
|
354
|
+
y = _col(df, "y")
|
|
322
355
|
model.fit(X, y=y)
|
|
323
356
|
|
|
324
357
|
inner = _inner_estimator(model)
|
|
325
358
|
|
|
326
|
-
if isinstance(
|
|
327
|
-
n_groups =
|
|
359
|
+
if isinstance(df, pl.DataFrame):
|
|
360
|
+
n_groups = df.select(pl.col("gameid").n_unique()).item()
|
|
328
361
|
else:
|
|
329
|
-
n_groups =
|
|
362
|
+
n_groups = df["gameid"].nunique()
|
|
330
363
|
|
|
331
364
|
assert inner.fit_shape[0] == n_groups
|
|
332
365
|
|
|
@@ -551,3 +584,300 @@ def test_autopipeline_is_picklable_after_fit():
|
|
|
551
584
|
model.fit(df, y)
|
|
552
585
|
|
|
553
586
|
pickle.dumps(model)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
# --- Feature Importances Tests ---
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def test_feature_importances__tree_model():
|
|
593
|
+
from sklearn.ensemble import RandomForestRegressor
|
|
594
|
+
|
|
595
|
+
df = pd.DataFrame(
|
|
596
|
+
{
|
|
597
|
+
"num1": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
|
|
598
|
+
"num2": [10.0, 20.0, 30.0, 40.0, 50.0, 60.0],
|
|
599
|
+
"cat1": ["a", "b", "a", "b", "a", "b"],
|
|
600
|
+
}
|
|
601
|
+
)
|
|
602
|
+
y = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], name="y")
|
|
603
|
+
|
|
604
|
+
model = AutoPipeline(
|
|
605
|
+
estimator=RandomForestRegressor(n_estimators=5, random_state=42),
|
|
606
|
+
estimator_features=["num1", "num2", "cat1"],
|
|
607
|
+
categorical_handling="ordinal",
|
|
608
|
+
)
|
|
609
|
+
model.fit(df, y)
|
|
610
|
+
|
|
611
|
+
importances = model.feature_importances_
|
|
612
|
+
|
|
613
|
+
assert isinstance(importances, pd.DataFrame)
|
|
614
|
+
assert list(importances.columns) == ["feature", "importance"]
|
|
615
|
+
assert len(importances) == 3
|
|
616
|
+
assert set(importances["feature"].tolist()) == {"num1", "num2", "cat1"}
|
|
617
|
+
assert all(importances["importance"] >= 0)
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def test_feature_importances__linear_model():
|
|
621
|
+
df = pd.DataFrame(
|
|
622
|
+
{
|
|
623
|
+
"num1": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0],
|
|
624
|
+
"num2": [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0],
|
|
625
|
+
}
|
|
626
|
+
)
|
|
627
|
+
y = pd.Series([0, 1, 0, 1, 0, 1, 0, 1], name="y")
|
|
628
|
+
|
|
629
|
+
model = AutoPipeline(
|
|
630
|
+
estimator=LogisticRegression(max_iter=1000),
|
|
631
|
+
estimator_features=["num1", "num2"],
|
|
632
|
+
scale_features=True,
|
|
633
|
+
)
|
|
634
|
+
model.fit(df, y)
|
|
635
|
+
|
|
636
|
+
importances = model.feature_importances_
|
|
637
|
+
|
|
638
|
+
assert isinstance(importances, pd.DataFrame)
|
|
639
|
+
assert list(importances.columns) == ["feature", "importance"]
|
|
640
|
+
assert len(importances) == 2
|
|
641
|
+
assert set(importances["feature"].tolist()) == {"num1", "num2"}
|
|
642
|
+
assert all(importances["importance"] >= 0)
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def test_feature_importances__not_fitted_raises():
|
|
646
|
+
model = AutoPipeline(
|
|
647
|
+
estimator=LinearRegression(),
|
|
648
|
+
estimator_features=["x"],
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
with pytest.raises(RuntimeError, match="Pipeline not fitted"):
|
|
652
|
+
_ = model.feature_importances_
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def test_feature_importances__unsupported_estimator_raises():
|
|
656
|
+
df = pd.DataFrame({"x": [1.0, 2.0, 3.0, 4.0]})
|
|
657
|
+
y = pd.Series([1.0, 2.0, 3.0, 4.0], name="y")
|
|
658
|
+
|
|
659
|
+
model = AutoPipeline(
|
|
660
|
+
estimator=DummyRegressor(),
|
|
661
|
+
estimator_features=["x"],
|
|
662
|
+
)
|
|
663
|
+
model.fit(df, y)
|
|
664
|
+
|
|
665
|
+
with pytest.raises(RuntimeError, match="does not support feature importances"):
|
|
666
|
+
_ = model.feature_importances_
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def test_feature_importances__with_sklearn_enhancer():
|
|
670
|
+
from sklearn.ensemble import RandomForestRegressor
|
|
671
|
+
|
|
672
|
+
df = pd.DataFrame(
|
|
673
|
+
{
|
|
674
|
+
"num1": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
|
|
675
|
+
"num2": [10.0, 20.0, 30.0, 40.0, 50.0, 60.0],
|
|
676
|
+
"start_date": ["2022-01-01", "2022-01-02", "2022-01-03", "2022-01-04", "2022-01-05", "2022-01-06"],
|
|
677
|
+
}
|
|
678
|
+
)
|
|
679
|
+
y = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], name="y")
|
|
680
|
+
|
|
681
|
+
inner = RandomForestRegressor(n_estimators=5, random_state=42)
|
|
682
|
+
enhancer = SkLearnEnhancerEstimator(
|
|
683
|
+
estimator=inner,
|
|
684
|
+
date_column="start_date",
|
|
685
|
+
day_weight_epsilon=0.1,
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
model = AutoPipeline(
|
|
689
|
+
estimator=enhancer,
|
|
690
|
+
estimator_features=["num1", "num2"],
|
|
691
|
+
)
|
|
692
|
+
model.fit(df, y)
|
|
693
|
+
|
|
694
|
+
importances = model.feature_importances_
|
|
695
|
+
|
|
696
|
+
assert isinstance(importances, pd.DataFrame)
|
|
697
|
+
assert list(importances.columns) == ["feature", "importance"]
|
|
698
|
+
assert len(importances) == 2
|
|
699
|
+
assert set(importances["feature"].tolist()) == {"num1", "num2"}
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def test_feature_importances__onehot_features():
|
|
703
|
+
from sklearn.ensemble import RandomForestRegressor
|
|
704
|
+
|
|
705
|
+
df = pd.DataFrame(
|
|
706
|
+
{
|
|
707
|
+
"num1": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
|
|
708
|
+
"cat1": ["a", "b", "c", "a", "b", "c"],
|
|
709
|
+
}
|
|
710
|
+
)
|
|
711
|
+
y = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], name="y")
|
|
712
|
+
|
|
713
|
+
model = AutoPipeline(
|
|
714
|
+
estimator=RandomForestRegressor(n_estimators=5, random_state=42),
|
|
715
|
+
estimator_features=["num1", "cat1"],
|
|
716
|
+
categorical_handling="onehot",
|
|
717
|
+
)
|
|
718
|
+
model.fit(df, y)
|
|
719
|
+
|
|
720
|
+
importances = model.feature_importances_
|
|
721
|
+
|
|
722
|
+
assert isinstance(importances, pd.DataFrame)
|
|
723
|
+
assert list(importances.columns) == ["feature", "importance"]
|
|
724
|
+
# Should have expanded features: num1 + cat1_a, cat1_b, cat1_c
|
|
725
|
+
assert len(importances) == 4
|
|
726
|
+
assert "num1" in importances["feature"].tolist()
|
|
727
|
+
assert any("cat1_" in f for f in importances["feature"].tolist())
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def test_feature_importance_names__granularity_uses_deep_feature_names():
|
|
731
|
+
from sklearn.ensemble import RandomForestRegressor
|
|
732
|
+
|
|
733
|
+
df = pd.DataFrame(
|
|
734
|
+
{
|
|
735
|
+
"gameid": ["g1", "g1", "g2", "g2"],
|
|
736
|
+
"num1": [1.0, 2.0, 3.0, 4.0],
|
|
737
|
+
"num2": [10.0, 20.0, 30.0, 40.0],
|
|
738
|
+
"y": [1.0, 1.0, 2.0, 2.0],
|
|
739
|
+
}
|
|
740
|
+
)
|
|
741
|
+
y = df["y"]
|
|
742
|
+
|
|
743
|
+
model = AutoPipeline(
|
|
744
|
+
estimator=RandomForestRegressor(n_estimators=5, random_state=42),
|
|
745
|
+
estimator_features=["gameid", "num1", "num2"],
|
|
746
|
+
predictor_transformers=[AddConstantPredictionTransformer(col_name="const_pred")],
|
|
747
|
+
granularity=["gameid"],
|
|
748
|
+
categorical_features=["gameid"],
|
|
749
|
+
categorical_handling="ordinal",
|
|
750
|
+
remainder="drop",
|
|
751
|
+
)
|
|
752
|
+
model.fit(df, y)
|
|
753
|
+
|
|
754
|
+
names = model.feature_importance_names
|
|
755
|
+
|
|
756
|
+
inner = _inner_estimator(model)
|
|
757
|
+
assert list(names.keys()) == list(inner.feature_names_in_)
|
|
758
|
+
assert "gameid" not in names
|
|
759
|
+
assert "const_pred" in names
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
@pytest.mark.parametrize("frame", ["pd", "pl"])
|
|
763
|
+
def test_granularity_with_aggregation_weight__features_weighted(frame):
|
|
764
|
+
df_pd = pd.DataFrame(
|
|
765
|
+
{
|
|
766
|
+
"gameid": ["g1", "g1", "g2", "g2"],
|
|
767
|
+
"num1": [10.0, 30.0, 20.0, 40.0],
|
|
768
|
+
"weight": [0.25, 0.75, 0.5, 0.5],
|
|
769
|
+
"y": [1.0, 1.0, 2.0, 2.0],
|
|
770
|
+
}
|
|
771
|
+
)
|
|
772
|
+
df = df_pd if frame == "pd" else pl.from_pandas(df_pd)
|
|
773
|
+
|
|
774
|
+
cap = CaptureEstimator()
|
|
775
|
+
model = AutoPipeline(
|
|
776
|
+
estimator=cap,
|
|
777
|
+
estimator_features=["num1"],
|
|
778
|
+
granularity=["gameid"],
|
|
779
|
+
aggregation_weight="weight",
|
|
780
|
+
remainder="drop",
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
X = _select(df, ["gameid", "num1", "weight"])
|
|
784
|
+
y = _col(df, "y")
|
|
785
|
+
model.fit(X, y=y)
|
|
786
|
+
|
|
787
|
+
inner = _inner_estimator(model)
|
|
788
|
+
assert inner.fit_shape[0] == 2
|
|
789
|
+
|
|
790
|
+
preds = model.predict(X)
|
|
791
|
+
assert preds.shape[0] == len(X)
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
@pytest.mark.parametrize("frame", ["pd", "pl"])
|
|
795
|
+
def test_granularity_aggregation_weight__weighted_mean_correct(frame):
|
|
796
|
+
df_pd = pd.DataFrame(
|
|
797
|
+
{
|
|
798
|
+
"gameid": ["g1", "g1"],
|
|
799
|
+
"num1": [10.0, 30.0],
|
|
800
|
+
"weight": [0.25, 0.75],
|
|
801
|
+
"y": [1.0, 1.0],
|
|
802
|
+
}
|
|
803
|
+
)
|
|
804
|
+
df = df_pd if frame == "pd" else pl.from_pandas(df_pd)
|
|
805
|
+
|
|
806
|
+
from spforge.transformers._other_transformer import GroupByReducer
|
|
807
|
+
|
|
808
|
+
reducer = GroupByReducer(granularity=["gameid"], aggregation_weight="weight")
|
|
809
|
+
transformed = reducer.fit_transform(df)
|
|
810
|
+
|
|
811
|
+
if frame == "pl":
|
|
812
|
+
num1_val = transformed["num1"].to_list()[0]
|
|
813
|
+
else:
|
|
814
|
+
num1_val = transformed["num1"].iloc[0]
|
|
815
|
+
|
|
816
|
+
expected = (10.0 * 0.25 + 30.0 * 0.75) / (0.25 + 0.75)
|
|
817
|
+
assert abs(num1_val - expected) < 1e-6
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
@pytest.mark.parametrize("frame", ["pd", "pl"])
|
|
821
|
+
def test_reduce_y_raises_when_target_not_uniform_per_group(frame):
|
|
822
|
+
df_pd = pd.DataFrame(
|
|
823
|
+
{
|
|
824
|
+
"gameid": ["g1", "g1"],
|
|
825
|
+
"num1": [10.0, 30.0],
|
|
826
|
+
}
|
|
827
|
+
)
|
|
828
|
+
df = df_pd if frame == "pd" else pl.from_pandas(df_pd)
|
|
829
|
+
|
|
830
|
+
from spforge.transformers._other_transformer import GroupByReducer
|
|
831
|
+
|
|
832
|
+
reducer = GroupByReducer(granularity=["gameid"])
|
|
833
|
+
|
|
834
|
+
y = np.array([1.0, 2.0])
|
|
835
|
+
with pytest.raises(ValueError, match="Target.*must be uniform"):
|
|
836
|
+
reducer.reduce_y(df, y)
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
@pytest.mark.parametrize("frame", ["pd", "pl"])
|
|
840
|
+
def test_reduce_y_works_when_target_uniform_per_group(frame):
|
|
841
|
+
df_pd = pd.DataFrame(
|
|
842
|
+
{
|
|
843
|
+
"gameid": ["g1", "g1", "g2", "g2"],
|
|
844
|
+
"num1": [10.0, 30.0, 20.0, 40.0],
|
|
845
|
+
}
|
|
846
|
+
)
|
|
847
|
+
df = df_pd if frame == "pd" else pl.from_pandas(df_pd)
|
|
848
|
+
|
|
849
|
+
from spforge.transformers._other_transformer import GroupByReducer
|
|
850
|
+
|
|
851
|
+
reducer = GroupByReducer(granularity=["gameid"])
|
|
852
|
+
|
|
853
|
+
y = np.array([1.0, 1.0, 2.0, 2.0])
|
|
854
|
+
y_out, _ = reducer.reduce_y(df, y)
|
|
855
|
+
|
|
856
|
+
assert len(y_out) == 2
|
|
857
|
+
assert set(y_out) == {1.0, 2.0}
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
@pytest.mark.parametrize("frame", ["pd", "pl"])
|
|
861
|
+
def test_aggregation_weight_sums_weight_column(frame):
|
|
862
|
+
df_pd = pd.DataFrame(
|
|
863
|
+
{
|
|
864
|
+
"gameid": ["g1", "g1"],
|
|
865
|
+
"num1": [10.0, 30.0],
|
|
866
|
+
"weight": [0.25, 0.75],
|
|
867
|
+
"y": [1.0, 1.0],
|
|
868
|
+
}
|
|
869
|
+
)
|
|
870
|
+
df = df_pd if frame == "pd" else pl.from_pandas(df_pd)
|
|
871
|
+
|
|
872
|
+
from spforge.transformers._other_transformer import GroupByReducer
|
|
873
|
+
|
|
874
|
+
reducer = GroupByReducer(granularity=["gameid"], aggregation_weight="weight")
|
|
875
|
+
transformed = reducer.fit_transform(df)
|
|
876
|
+
|
|
877
|
+
if frame == "pl":
|
|
878
|
+
weight_val = transformed["weight"].to_list()[0]
|
|
879
|
+
else:
|
|
880
|
+
weight_val = transformed["weight"].iloc[0]
|
|
881
|
+
|
|
882
|
+
expected = 0.25 + 0.75
|
|
883
|
+
assert abs(weight_val - expected) < 1e-6
|
|
File without changes
|
|
File without changes
|