Files
nixpkgs/pkgs/development/python-modules/mlxtend/fix-sklearn-1.9-tests.patch

285 lines
12 KiB
Diff

From 93ac19f44b8e9688ea76459bb523a964a1dab2b6 Mon Sep 17 00:00:00 2001
From: Sebastian Raschka <rasbt@users.noreply.github.com>
Date: Mon, 28 Sep 2026 13:28:20 -0500
Subject: [PATCH] Fix sklearn 1.9 tests (#1205)
---
.github/workflows/python-package-uv.yml | 33 +++++++
.../tests/test_ensemble_vote_classifier.py | 7 +-
.../tests/test_stacking_classifier.py | 7 +-
.../tests/test_stacking_cv_classifier.py | 7 +-
.../tests/test_exhaustive_feature_selector.py | 85 ++++++++-----------
5 files changed, 80 insertions(+), 59 deletions(-)
diff --git a/.github/workflows/python-package-uv.yml b/.github/workflows/python-package-uv.yml
index ce869c72c..4bc0c6077 100644
--- a/.github/workflows/python-package-uv.yml
+++ b/.github/workflows/python-package-uv.yml
@@ -41,3 +41,36 @@ jobs:
- name: Upload Coverage to Codecov
uses: codecov/codecov-action@v4
+
+ build-linux-latest-sklearn:
+ runs-on: ubuntu-latest
+ env:
+ GITHUB_ACTIONS_CI: true
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
+
+ - name: Install dependencies
+ run: uv sync --frozen --group dev --python 3.11
+
+ - name: Upgrade scikit-learn
+ run: >-
+ uv pip install --python .venv/bin/python
+ --upgrade-package scikit-learn --prerelease disallow scikit-learn
+
+ - name: Show dependency versions
+ run: |
+ uv run --no-sync python -c "import numpy; print('NumPy:', numpy.__version__)"
+ uv run --no-sync python -c "import scipy; print('SciPy:', scipy.__version__)"
+ uv run --no-sync python -c "import sklearn; print('Scikit-learn:', sklearn.__version__)"
+ uv run --no-sync python -c "import pandas; print('Pandas:', pandas.__version__)"
+
+ - name: Run tests
+ run: uv run --no-sync python -m pytest mlxtend
diff --git a/mlxtend/classifier/tests/test_ensemble_vote_classifier.py b/mlxtend/classifier/tests/test_ensemble_vote_classifier.py
index a0792576d..62ea57ded 100644
--- a/mlxtend/classifier/tests/test_ensemble_vote_classifier.py
+++ b/mlxtend/classifier/tests/test_ensemble_vote_classifier.py
@@ -78,10 +78,11 @@ def test_use_clones():
def test_sample_weight():
+ # Disable bootstrap sampling to compare unit weights with no weights.
# with no weight
np.random.seed(123)
clf1 = LogisticRegression(solver="lbfgs", max_iter=500)
- clf2 = RandomForestClassifier(n_estimators=10)
+ clf2 = RandomForestClassifier(n_estimators=10, bootstrap=False, random_state=123)
clf3 = GaussianNB()
eclf = EnsembleVoteClassifier(clfs=[clf1, clf2, clf3], voting="hard")
prob1 = eclf.fit(X, y).predict_proba(X)
@@ -90,7 +91,7 @@ def test_sample_weight():
w = np.ones(len(y))
np.random.seed(123)
clf1 = LogisticRegression(solver="lbfgs", max_iter=500)
- clf2 = RandomForestClassifier(n_estimators=10)
+ clf2 = RandomForestClassifier(n_estimators=10, bootstrap=False, random_state=123)
clf3 = GaussianNB()
eclf = EnsembleVoteClassifier(clfs=[clf1, clf2, clf3], voting="hard")
prob2 = eclf.fit(X, y, sample_weight=w).predict_proba(X)
@@ -100,7 +101,7 @@ def test_sample_weight():
w = np.array([random.random() for _ in range(len(y))])
np.random.seed(123)
clf1 = LogisticRegression(solver="lbfgs", max_iter=500)
- clf2 = RandomForestClassifier(n_estimators=10)
+ clf2 = RandomForestClassifier(n_estimators=10, bootstrap=False, random_state=123)
clf3 = GaussianNB()
eclf = EnsembleVoteClassifier(clfs=[clf1, clf2, clf3], voting="hard")
prob3 = eclf.fit(X, y, sample_weight=w).predict_proba(X)
diff --git a/mlxtend/classifier/tests/test_stacking_classifier.py b/mlxtend/classifier/tests/test_stacking_classifier.py
index b9a0f7f5e..49016f9c5 100644
--- a/mlxtend/classifier/tests/test_stacking_classifier.py
+++ b/mlxtend/classifier/tests/test_stacking_classifier.py
@@ -97,6 +97,7 @@ def test_use_clones():
def test_sample_weight():
+ # Disable bootstrap sampling to compare unit weights with no weights.
# Make sure that:
# prediction with weight
# != prediction with no weight
@@ -106,14 +107,14 @@ def test_sample_weight():
np.random.seed(123)
meta = LogisticRegression(solver="lbfgs")
- clf1 = RandomForestClassifier(n_estimators=10)
+ clf1 = RandomForestClassifier(n_estimators=10, bootstrap=False, random_state=123)
clf2 = GaussianNB()
sclf = StackingClassifier(classifiers=[clf1, clf2], meta_classifier=meta)
prob1 = sclf.fit(X, y, sample_weight=w).predict_proba(X)
np.random.seed(123)
meta = LogisticRegression(solver="lbfgs")
- clf1 = RandomForestClassifier(n_estimators=10)
+ clf1 = RandomForestClassifier(n_estimators=10, bootstrap=False, random_state=123)
clf2 = GaussianNB()
sclf = StackingClassifier(classifiers=[clf1, clf2], meta_classifier=meta)
prob2 = sclf.fit(X, y, sample_weight=None).predict_proba(X)
@@ -123,7 +124,7 @@ def test_sample_weight():
np.random.seed(123)
meta = LogisticRegression(solver="lbfgs")
- clf1 = RandomForestClassifier(n_estimators=10)
+ clf1 = RandomForestClassifier(n_estimators=10, bootstrap=False, random_state=123)
clf2 = GaussianNB()
sclf = StackingClassifier(classifiers=[clf1, clf2], meta_classifier=meta)
prob3 = sclf.fit(X, y, sample_weight=np.ones(len(y))).predict_proba(X)
diff --git a/mlxtend/classifier/tests/test_stacking_cv_classifier.py b/mlxtend/classifier/tests/test_stacking_cv_classifier.py
index aa1f76ec0..47d8cebf5 100644
--- a/mlxtend/classifier/tests/test_stacking_cv_classifier.py
+++ b/mlxtend/classifier/tests/test_stacking_cv_classifier.py
@@ -90,10 +90,11 @@ def test_use_clones():
def test_sample_weight():
+ # Disable bootstrap sampling to compare unit weights with no weights.
# with no weight given
np.random.seed(123)
meta = LogisticRegression(solver="lbfgs")
- clf1 = RandomForestClassifier(n_estimators=10)
+ clf1 = RandomForestClassifier(n_estimators=10, bootstrap=False, random_state=123)
clf2 = GaussianNB()
sclf = StackingCVClassifier(
classifiers=[clf1, clf2], meta_classifier=meta, shuffle=False
@@ -103,7 +104,7 @@ def test_sample_weight():
# with weight = 1
np.random.seed(123)
meta = LogisticRegression(solver="lbfgs")
- clf1 = RandomForestClassifier(n_estimators=10)
+ clf1 = RandomForestClassifier(n_estimators=10, bootstrap=False, random_state=123)
clf2 = GaussianNB()
sclf = StackingCVClassifier(
classifiers=[clf1, clf2], meta_classifier=meta, shuffle=False
@@ -116,7 +117,7 @@ def test_sample_weight():
w = np.array([random.random() for _ in range(len(y_iris))])
np.random.seed(123)
meta = LogisticRegression(solver="lbfgs")
- clf1 = RandomForestClassifier(n_estimators=10)
+ clf1 = RandomForestClassifier(n_estimators=10, bootstrap=False, random_state=123)
clf2 = GaussianNB()
sclf = StackingCVClassifier(
classifiers=[clf1, clf2], meta_classifier=meta, shuffle=False
diff --git a/mlxtend/feature_selection/tests/test_exhaustive_feature_selector.py b/mlxtend/feature_selection/tests/test_exhaustive_feature_selector.py
index 803f79c44..5c2a4cac6 100644
--- a/mlxtend/feature_selection/tests/test_exhaustive_feature_selector.py
+++ b/mlxtend/feature_selection/tests/test_exhaustive_feature_selector.py
@@ -4,15 +4,19 @@
#
# License: BSD 3 clause
+from itertools import combinations
+
import numpy as np
import pandas as pd
+import pytest
from numpy.testing import assert_almost_equal
from packaging.version import Version
from sklearn import __version__ as sklearn_version
+from sklearn.base import clone
from sklearn.datasets import load_iris
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LinearRegression, LogisticRegression
-from sklearn.model_selection import GroupKFold
+from sklearn.model_selection import GroupKFold, StratifiedKFold
from sklearn.neighbors import KNeighborsClassifier
from mlxtend.classifier import SoftmaxRegression
@@ -269,11 +273,16 @@ def test_knn_cv3_groups():
dict_compare_utility(d1=expect, d2=efs1.subsets_)
-def test_fit_params():
+@pytest.mark.parametrize("uniform_weights", [True, False])
+def test_fit_params(uniform_weights):
iris = load_iris()
X = iris.data
y = iris.target
- sample_weight = np.ones(X.shape[0])
+ if uniform_weights:
+ sample_weight = np.ones(X.shape[0])
+ else:
+ sample_weight = np.random.RandomState(87).uniform(0.05, 2.0, size=X.shape[0])
+
forest = RandomForestClassifier(n_estimators=100, random_state=123)
efs1 = EFS(
forest,
@@ -284,54 +293,30 @@ def test_fit_params():
print_progress=False,
)
efs1 = efs1.fit(X, y, sample_weight=sample_weight)
- expect = {
- 0: {
- "feature_idx": (0, 1, 2),
- "feature_names": ("0", "1", "2"),
- "cv_scores": np.array([0.947, 0.868, 0.919, 0.973]),
- "avg_score": 0.9269203413940257,
- },
- 1: {
- "feature_idx": (0, 1, 3),
- "feature_names": ("0", "1", "3"),
- "cv_scores": np.array([0.921, 0.921, 0.892, 1.0]),
- "avg_score": 0.9337606837606838,
- },
- 2: {
- "feature_idx": (0, 2, 3),
- "feature_names": ("0", "2", "3"),
- "cv_scores": np.array([0.974, 0.947, 0.919, 0.973]),
- "avg_score": 0.9532361308677098,
- },
- 3: {
- "feature_idx": (1, 2, 3),
- "feature_names": ("1", "2", "3"),
- "cv_scores": np.array([0.974, 0.947, 0.892, 1.0]),
- "avg_score": 0.9532361308677098,
- },
- }
- if Version(sklearn_version) < Version("0.22"):
- expect[0]["avg_score"] = 0.9401709401709402
- expect[0]["cv_scores"] = np.array(
- [0.94871795, 0.92307692, 0.91666667, 0.97222222]
- )
- expect[1]["cv_scores"] = np.array(
- [0.94871795, 0.92307692, 0.91666667, 0.97222222]
- )
- expect[2]["cv_scores"] = np.array(
- [0.94871795, 0.92307692, 0.91666667, 0.97222222]
- )
- expect[2]["avg_score"] = 0.9599358974358974
- expect[3]["avg_score"] = 0.9599358974358974
- expect[3]["cv_scores"] = np.array([0.97435897, 0.94871795, 0.91666667, 1.0])
- assert round(efs1.best_score_, 4) == 0.9599
-
- else:
- assert round(efs1.best_score_, 4) == 0.9532
-
- dict_compare_utility(d1=expect, d2=efs1.subsets_)
- assert efs1.best_idx_ == (0, 2, 3)
+ # Explicit fold fits check that weights reach the correct training rows
+ # without depending on a particular version's bootstrap sampling results.
+ expect = {}
+ cv = StratifiedKFold(n_splits=4)
+ for i, feature_idx in enumerate(combinations(range(X.shape[1]), 3)):
+ X_subset = X[:, feature_idx]
+ cv_scores = []
+ for train, test in cv.split(X, y):
+ fitted = clone(forest).fit(
+ X_subset[train], y[train], sample_weight=sample_weight[train]
+ )
+ cv_scores.append(fitted.score(X_subset[test], y[test]))
+ expect[i] = {
+ "feature_idx": feature_idx,
+ "feature_names": tuple(str(idx) for idx in feature_idx),
+ "cv_scores": np.array(cv_scores),
+ "avg_score": np.mean(cv_scores),
+ }
+
+ dict_compare_utility(d1=expect, d2=efs1.subsets_, decimal=7)
+ best_subset = max(expect.values(), key=lambda subset: subset["avg_score"])
+ assert_almost_equal(efs1.best_score_, best_subset["avg_score"])
+ assert efs1.best_idx_ == best_subset["feature_idx"]
def test_regression():