From 3ad8c2857416e14d5c94dc00a693c44bd443961a Mon Sep 17 00:00:00 2001 From: Genuster Date: Wed, 30 Jul 2025 18:42:22 +0300 Subject: [PATCH 01/20] initial improvements --- mne/decoding/base.py | 103 +++++++++++++++++++++++-------------------- 1 file changed, 55 insertions(+), 48 deletions(-) diff --git a/mne/decoding/base.py b/mne/decoding/base.py index e6de710c618..03b344bf204 100644 --- a/mne/decoding/base.py +++ b/mne/decoding/base.py @@ -17,12 +17,13 @@ TransformerMixin, clone, is_classifier, + is_regressor, ) from sklearn.linear_model import LogisticRegression from sklearn.metrics import check_scoring from sklearn.model_selection import KFold, StratifiedKFold, check_cv -from sklearn.utils import check_array, check_X_y, indexable -from sklearn.utils.validation import check_is_fitted +from sklearn.utils import get_tags, indexable +from sklearn.utils.validation import check_is_fitted, validate_data from ..parallel import parallel_func from ..utils import _check_option, _pl, _validate_type, logger, pinv, verbose, warn @@ -340,7 +341,8 @@ class LinearModel(MetaEstimatorMixin, BaseEstimator): model : object | None A linear model from scikit-learn with a fit method that updates a ``coef_`` attribute. - If None the model will be LogisticRegression. + If None the model will be + :class:`sklearn.linear_model.LogisticRegression`. Attributes ---------- @@ -364,46 +366,48 @@ class LinearModel(MetaEstimatorMixin, BaseEstimator): .. footbibliography:: """ - # TODO: Properly refactor this using - # https://github.com/scikit-learn/scikit-learn/issues/30237#issuecomment-2465572885 - _model_attr_wrap = ( - "transform", - "predict", - "predict_proba", - "_estimator_type", - "__tags__", - "decision_function", - "score", - "classes_", - ) - def __init__(self, model=None): - # TODO: We need to set this to get our tag checking to work properly if model is None: model = LogisticRegression(solver="liblinear") self.model = model def __sklearn_tags__(self): """Get sklearn tags.""" - from sklearn.utils import get_tags # added in 1.6 - - # fit method below does not allow sparse data via check_data, we could - # eventually make it smarter if we had to - tags = get_tags(self.model) - tags.input_tags.sparse = False + tags = super().__sklearn_tags__() + model_tags = get_tags(self.model) + tags.estimator_type = model_tags.estimator_type + model_type_tags = getattr(model_tags, f"{tags.estimator_type}_tags") + setattr(tags, f"{tags.estimator_type}_tags", model_type_tags) return tags - def __getattr__(self, attr): - """Wrap to model for some attributes.""" - if attr in LinearModel._model_attr_wrap: - return getattr(self.model, attr) - elif attr == "fit_transform" and hasattr(self.model, "fit_transform"): - return super().__getattr__(self, "_fit_transform") - return super().__getattr__(self, attr) - def _fit_transform(self, X, y): return self.fit(X, y).transform(X) + def _validate_params(self): + model_type = self.__sklearn_tags__().estimator_type + if model_type not in ("classifier", "regressor"): + raise ValueError( + "Linear model should be a supervised predictor " + "(classifier or regressor)" + ) + if hasattr(self.model, "score"): + self.score = self.model.score + if hasattr(self.model, "transform"): + self.transform = self.model.transform + if hasattr(self.model, "fit_transform"): + self.fit_transform = self._fit_transform + + if model_type == "classifier": + classifer_methods = { + "predict_proba", + "predict_log_proba", + "decision_function", + } + for method_name in classifer_methods: + if hasattr(self.model, method_name): + method = getattr(self.model, method_name) + setattr(self, method_name, method) + def fit(self, X, y, **fit_params): """Estimate the coefficients of the linear model. @@ -424,23 +428,12 @@ def fit(self, X, y, **fit_params): self : instance of LinearModel Returns the modified instance. """ - if y is not None: - X = check_array(X) - else: - X, y = check_X_y(X, y) - self.n_features_in_ = X.shape[1] - if y is not None: - y = check_array(y, dtype=None, ensure_2d=False, input_name="y") - if y.ndim > 2: - raise ValueError( - f"LinearModel only accepts up to 2-dimensional y, got {y.shape} " - "instead." - ) + self._validate_params() + X, y = validate_data(self, X, y, multi_output=True) # fit the Model self.model.fit(X, y, **fit_params) self.model_ = self.model # for better sklearn compat - # Computes patterns using Haufe's trick: A = Cov_X . W . Precision_Y inv_Y = 1.0 @@ -452,20 +445,34 @@ def fit(self, X, y, **fit_params): return self + def predict(self, X): + """...""" + return self.model.predict(X) + @property def filters_(self): - if hasattr(self.model, "coef_"): + check_is_fitted(self) + if hasattr(self.model_, "coef_"): # Standard Linear Model - filters = self.model.coef_ - elif hasattr(self.model.best_estimator_, "coef_"): + filters = self.model_.coef_ + elif hasattr(self.model_.best_estimator_, "coef_"): # Linear Model with GridSearchCV - filters = self.model.best_estimator_.coef_ + filters = self.model_.best_estimator_.coef_ else: raise ValueError("model does not have a `coef_` attribute.") if filters.ndim == 2 and filters.shape[0] == 1: filters = filters[0] return filters + @property + def classes_(self): + check_is_fitted(self.model_) + if is_regressor(self.model_): + raise AttributeError("Regressors don't have the 'classes_' attribute") + elif hasattr(self.model_, "classes_"): + return self.model_.classes_ + return None + def _set_cv(cv, estimator=None, X=None, y=None): """Set the default CV depending on whether clf is classifier/regressor.""" From a3479cfe0657232438017ea8045fd5229262e08d Mon Sep 17 00:00:00 2001 From: Genuster Date: Wed, 30 Jul 2025 21:17:17 +0300 Subject: [PATCH 02/20] clone(model) instead of modifying it, change default model, add deprecation warnings. --- mne/decoding/base.py | 97 ++++++++++++++++++-------- mne/decoding/tests/test_base.py | 27 +++---- mne/decoding/tests/test_transformer.py | 7 +- 3 files changed, 89 insertions(+), 42 deletions(-) diff --git a/mne/decoding/base.py b/mne/decoding/base.py index 03b344bf204..4d312c6918b 100644 --- a/mne/decoding/base.py +++ b/mne/decoding/base.py @@ -26,7 +26,15 @@ from sklearn.utils.validation import check_is_fitted, validate_data from ..parallel import parallel_func -from ..utils import _check_option, _pl, _validate_type, logger, pinv, verbose, warn +from ..utils import ( + _check_option, + _pl, + _validate_type, + logger, + pinv, + verbose, + warn, +) from ._ged import ( _handle_restr_mat, _is_cov_pos_semidef, @@ -366,20 +374,49 @@ class LinearModel(MetaEstimatorMixin, BaseEstimator): .. footbibliography:: """ + _model_attr_wrap = ( + "transform", + "predict_proba", + "predict_log_proba", + "decision_function", + "score", + "model", + ) + def __init__(self, model=None): - if model is None: - model = LogisticRegression(solver="liblinear") self.model = model + # XXX Remove the clause after warning cycle + if model is None: + self.model = LogisticRegression(solver="liblinear") + depr_message = ( + "Starting with mne-python v1.12 'model' default " + "will change from LogisticRegression to None. " + "From now on please set model=LogisticRegression" + "(solver='liblinear') explicitly." + ) + warn(depr_message, FutureWarning) + def __sklearn_tags__(self): """Get sklearn tags.""" tags = super().__sklearn_tags__() - model_tags = get_tags(self.model) + # XXX Change self._orig_model to self.model after 'model' warning cycle + model_tags = get_tags(self._orig_model) tags.estimator_type = model_tags.estimator_type - model_type_tags = getattr(model_tags, f"{tags.estimator_type}_tags") - setattr(tags, f"{tags.estimator_type}_tags", model_type_tags) + if tags.estimator_type is not None: + model_type_tags = getattr(model_tags, f"{tags.estimator_type}_tags") + setattr(tags, f"{tags.estimator_type}_tags", model_type_tags) return tags + def __getattr__(self, attr): + """Wrap to model for some attributes.""" + model = self.model_ if "model_" in self.__dict__ else self.model + if attr in LinearModel._model_attr_wrap: + return getattr(model, attr) + elif attr == "fit_transform" and hasattr(model, "fit_transform"): + return super().__getattr__(self, "_fit_transform") + return super().__getattr__(self, attr) + def _fit_transform(self, X, y): return self.fit(X, y).transform(X) @@ -390,23 +427,6 @@ def _validate_params(self): "Linear model should be a supervised predictor " "(classifier or regressor)" ) - if hasattr(self.model, "score"): - self.score = self.model.score - if hasattr(self.model, "transform"): - self.transform = self.model.transform - if hasattr(self.model, "fit_transform"): - self.fit_transform = self._fit_transform - - if model_type == "classifier": - classifer_methods = { - "predict_proba", - "predict_log_proba", - "decision_function", - } - for method_name in classifer_methods: - if hasattr(self.model, method_name): - method = getattr(self.model, method_name) - setattr(self, method_name, method) def fit(self, X, y, **fit_params): """Estimate the coefficients of the linear model. @@ -432,10 +452,11 @@ def fit(self, X, y, **fit_params): X, y = validate_data(self, X, y, multi_output=True) # fit the Model - self.model.fit(X, y, **fit_params) - self.model_ = self.model # for better sklearn compat - # Computes patterns using Haufe's trick: A = Cov_X . W . Precision_Y + # XXX Change self._orig_model to self.model after 'model' warning cycle + self.model_ = clone(self._orig_model) + self.model_.fit(X, y, **fit_params) + # Computes patterns using Haufe's trick: A = Cov_X . W . Precision_Y inv_Y = 1.0 X = X - X.mean(0, keepdims=True) if y.ndim == 2 and y.shape[1] != 1: @@ -447,7 +468,8 @@ def fit(self, X, y, **fit_params): def predict(self, X): """...""" - return self.model.predict(X) + check_is_fitted(self) + return self.model_.predict(X) @property def filters_(self): @@ -466,13 +488,32 @@ def filters_(self): @property def classes_(self): - check_is_fitted(self.model_) + check_is_fitted(self) if is_regressor(self.model_): raise AttributeError("Regressors don't have the 'classes_' attribute") elif hasattr(self.model_, "classes_"): return self.model_.classes_ return None + # XXX Remove this property after 'model' warning cycle + @property + def model(self): + if "model_" in self.__dict__: + depr_message = ( + "Starting with mne-python v1.12 'model' attribute " + "of LinearModel will not be fitted, " + "please use 'model_' instead" + ) + warn(depr_message, FutureWarning) + return self.model_ + else: + return self._orig_model + + # XXX Remove this after 'model' warning cycle + @model.setter + def model(self, value): + self._orig_model = value + def _set_cv(cv, estimator=None, X=None, y=None): """Set the default CV depending on whether clf is classifier/regressor.""" diff --git a/mne/decoding/tests/test_base.py b/mne/decoding/tests/test_base.py index f17d4328279..fb1d9240d2e 100644 --- a/mne/decoding/tests/test_base.py +++ b/mne/decoding/tests/test_base.py @@ -96,7 +96,7 @@ def _make_data(n_samples=1000, n_features=5, n_targets=3): @pytest.mark.filterwarnings("ignore:invalid value encountered in cast.*:RuntimeWarning") def test_get_coef(): """Test getting linear coefficients (filters/patterns) from estimators.""" - lm_classification = LinearModel() + lm_classification = LinearModel(LogisticRegression(solver="liblinear")) assert hasattr(lm_classification, "__sklearn_tags__") if check_version("sklearn", "1.4"): print(lm_classification.__sklearn_tags__()) @@ -200,19 +200,19 @@ def inverse_transform(self, X): # Retrieve final linear model filters = get_coef(clf, "filters_", False) if hasattr(clf, "steps"): - if hasattr(clf.steps[-1][-1].model, "best_estimator_"): + if hasattr(clf.steps[-1][-1].model_, "best_estimator_"): # Linear Model with GridSearchCV - coefs = clf.steps[-1][-1].model.best_estimator_.coef_ + coefs = clf.steps[-1][-1].model_.best_estimator_.coef_ else: # Standard Linear Model - coefs = clf.steps[-1][-1].model.coef_ + coefs = clf.steps[-1][-1].model_.coef_ else: - if hasattr(clf.model, "best_estimator_"): + if hasattr(clf.model_, "best_estimator_"): # Linear Model with GridSearchCV - coefs = clf.model.best_estimator_.coef_ + coefs = clf.model_.best_estimator_.coef_ else: # Standard Linear Model - coefs = clf.model.coef_ + coefs = clf.model_.coef_ if coefs.ndim == 2 and coefs.shape[0] == 1: coefs = coefs[0] assert_array_equal(filters, coefs) @@ -280,9 +280,8 @@ def test_get_coef_multiclass(n_features, n_targets): lm = LinearModel(LinearRegression()) assert not hasattr(lm, "model_") lm.fit(X, Y) - # TODO: modifying non-underscored `model` is a sklearn no-no, maybe should be a - # metaestimator? - assert lm.model is lm.model_ + with pytest.warns(FutureWarning, match="'model' attribute of LinearModel"): + assert lm.model is lm.model_ assert_array_equal(lm.filters_.shape, lm.patterns_.shape) if n_targets == 1: want_shape = (n_features,) @@ -371,7 +370,7 @@ def test_linearmodel(): """Test LinearModel class for computing filters and patterns.""" # check categorical target fit in standard linear model rng = np.random.RandomState(0) - clf = LinearModel() + clf = LinearModel(LogisticRegression(solver="liblinear")) n, n_features = 20, 3 X = rng.rand(n, n_features) y = np.arange(n) % 2 @@ -478,12 +477,14 @@ def test_cross_val_multiscore(): assert_array_equal(manual, auto) +# XXX Remove filterwarning after 'model' warning cycle +@pytest.mark.filterwarnings("ignore::FutureWarning") @parametrize_with_checks([LinearModel(LogisticRegression())]) def test_sklearn_compliance(estimator, check): """Test LinearModel compliance with sklearn.""" + # XXX Remove the ignores after 'model' and default warning cycles ignores = ( - "check_estimators_overwrite_params", # self.model changes! - "check_dont_overwrite_parameters", + "check_estimators_overwrite_params", "check_parameters_default_constructible", ) if any(ignore in str(check) for ignore in ignores): diff --git a/mne/decoding/tests/test_transformer.py b/mne/decoding/tests/test_transformer.py index b6d97d3e435..6df3405c297 100644 --- a/mne/decoding/tests/test_transformer.py +++ b/mne/decoding/tests/test_transformer.py @@ -17,6 +17,7 @@ from sklearn.decomposition import PCA from sklearn.kernel_ridge import KernelRidge +from sklearn.linear_model import LogisticRegression from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler from sklearn.utils.estimator_checks import parametrize_with_checks @@ -229,7 +230,11 @@ def test_vectorizer(): # And that pipelines work properly X_arr = EpochsArray(X, create_info(12, 1000.0, "eeg")) vect.fit(X_arr) - clf = make_pipeline(Vectorizer(), StandardScaler(), LinearModel()) + clf = make_pipeline( + Vectorizer(), + StandardScaler(), + LinearModel(LogisticRegression(solver="liblinear")), + ) clf.fit(X_arr, y) From 9346f72e77fb1f30714f2d5322f661c834bd3c5f Mon Sep 17 00:00:00 2001 From: Genuster Date: Wed, 6 Aug 2025 11:06:40 +0300 Subject: [PATCH 03/20] make linearmodel work with onevsrestclassifier --- mne/decoding/base.py | 18 +++++++++++++----- mne/decoding/tests/test_base.py | 6 ++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/mne/decoding/base.py b/mne/decoding/base.py index 4d312c6918b..ba9a846ef58 100644 --- a/mne/decoding/base.py +++ b/mne/decoding/base.py @@ -376,6 +376,7 @@ class LinearModel(MetaEstimatorMixin, BaseEstimator): _model_attr_wrap = ( "transform", + "fit_transform", "predict_proba", "predict_log_proba", "decision_function", @@ -410,12 +411,16 @@ def __sklearn_tags__(self): def __getattr__(self, attr): """Wrap to model for some attributes.""" - model = self.model_ if "model_" in self.__dict__ else self.model if attr in LinearModel._model_attr_wrap: - return getattr(model, attr) - elif attr == "fit_transform" and hasattr(model, "fit_transform"): - return super().__getattr__(self, "_fit_transform") - return super().__getattr__(self, attr) + model = self.model_ if "model_" in self.__dict__ else self.model + if attr == "fit_transform" and hasattr(model, "fit_transform"): + return self._fit_transform + else: + return getattr(model, attr) + else: + raise AttributeError( + f"'{type(self).__name__}' object has no attribute '{attr}'" + ) def _fit_transform(self, X, y): return self.fit(X, y).transform(X) @@ -477,6 +482,9 @@ def filters_(self): if hasattr(self.model_, "coef_"): # Standard Linear Model filters = self.model_.coef_ + elif hasattr(self.model_, "estimators_"): + # Linear model with OneVsRestClassifier + filters = np.vstack([est.coef_ for est in self.model_.estimators_]) elif hasattr(self.model_.best_estimator_, "coef_"): # Linear Model with GridSearchCV filters = self.model_.best_estimator_.coef_ diff --git a/mne/decoding/tests/test_base.py b/mne/decoding/tests/test_base.py index fb1d9240d2e..a77b35518af 100644 --- a/mne/decoding/tests/test_base.py +++ b/mne/decoding/tests/test_base.py @@ -35,6 +35,7 @@ StratifiedKFold, cross_val_score, ) +from sklearn.multiclass import OneVsRestClassifier from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler from sklearn.utils.estimator_checks import parametrize_with_checks @@ -327,9 +328,6 @@ def test_get_coef_multiclass(n_features, n_targets): (3, 1, 2), ], ) -# TODO: Need to fix this properly in LinearModel -@pytest.mark.filterwarnings("ignore:'multi_class' was depr.*:FutureWarning") -@pytest.mark.filterwarnings("ignore:lbfgs failed to converge.*:") def test_get_coef_multiclass_full(n_classes, n_channels, n_times): """Test a full example with pattern extraction.""" data = np.zeros((10 * n_classes, n_channels, n_times)) @@ -344,7 +342,7 @@ def test_get_coef_multiclass_full(n_classes, n_channels, n_times): clf = make_pipeline( Scaler(epochs.info), Vectorizer(), - LinearModel(LogisticRegression(random_state=0, multi_class="ovr")), + LinearModel(OneVsRestClassifier(LogisticRegression(random_state=0))), ) scorer = "roc_auc_ovr_weighted" time_gen = GeneralizingEstimator(clf, scorer, verbose=True) From 4e2bcbda0d765405f8b3f1173de9c679031ccb31 Mon Sep 17 00:00:00 2001 From: Genuster Date: Wed, 6 Aug 2025 11:39:41 +0300 Subject: [PATCH 04/20] few last fixes --- mne/decoding/base.py | 7 ++++--- mne/decoding/tests/test_base.py | 5 ++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mne/decoding/base.py b/mne/decoding/base.py index ba9a846ef58..e808ceee5cc 100644 --- a/mne/decoding/base.py +++ b/mne/decoding/base.py @@ -391,7 +391,7 @@ def __init__(self, model=None): if model is None: self.model = LogisticRegression(solver="liblinear") depr_message = ( - "Starting with mne-python v1.12 'model' default " + "Starting with mne-python v1.13 'model' default " "will change from LogisticRegression to None. " "From now on please set model=LogisticRegression" "(solver='liblinear') explicitly." @@ -412,7 +412,8 @@ def __sklearn_tags__(self): def __getattr__(self, attr): """Wrap to model for some attributes.""" if attr in LinearModel._model_attr_wrap: - model = self.model_ if "model_" in self.__dict__ else self.model + # XXX Change self._orig_model to self.model after 'model' warning cycle + model = self.model_ if "model_" in self.__dict__ else self._orig_model if attr == "fit_transform" and hasattr(model, "fit_transform"): return self._fit_transform else: @@ -508,7 +509,7 @@ def classes_(self): def model(self): if "model_" in self.__dict__: depr_message = ( - "Starting with mne-python v1.12 'model' attribute " + "Starting with mne-python v1.13 'model' attribute " "of LinearModel will not be fitted, " "please use 'model_' instead" ) diff --git a/mne/decoding/tests/test_base.py b/mne/decoding/tests/test_base.py index a77b35518af..de85672036b 100644 --- a/mne/decoding/tests/test_base.py +++ b/mne/decoding/tests/test_base.py @@ -94,7 +94,6 @@ def _make_data(n_samples=1000, n_features=5, n_targets=3): return X, Y, A -@pytest.mark.filterwarnings("ignore:invalid value encountered in cast.*:RuntimeWarning") def test_get_coef(): """Test getting linear coefficients (filters/patterns) from estimators.""" lm_classification = LinearModel(LogisticRegression(solver="liblinear")) @@ -475,12 +474,12 @@ def test_cross_val_multiscore(): assert_array_equal(manual, auto) -# XXX Remove filterwarning after 'model' warning cycle +# XXX Remove the filterwarning after 'model' warning cycle @pytest.mark.filterwarnings("ignore::FutureWarning") @parametrize_with_checks([LinearModel(LogisticRegression())]) def test_sklearn_compliance(estimator, check): """Test LinearModel compliance with sklearn.""" - # XXX Remove the ignores after 'model' and default warning cycles + # XXX Remove the ignores after 'model' warning cycle ignores = ( "check_estimators_overwrite_params", "check_parameters_default_constructible", From 568f0ea37da44017339ad8a625b4c5341220affe Mon Sep 17 00:00:00 2001 From: Genuster Date: Wed, 6 Aug 2025 11:48:56 +0300 Subject: [PATCH 05/20] add futurewarning test --- mne/decoding/base.py | 7 +++---- mne/decoding/tests/test_base.py | 2 ++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/mne/decoding/base.py b/mne/decoding/base.py index e808ceee5cc..7c2740bc347 100644 --- a/mne/decoding/base.py +++ b/mne/decoding/base.py @@ -381,15 +381,12 @@ class LinearModel(MetaEstimatorMixin, BaseEstimator): "predict_log_proba", "decision_function", "score", - "model", ) def __init__(self, model=None): - self.model = model - # XXX Remove the clause after warning cycle if model is None: - self.model = LogisticRegression(solver="liblinear") + model = LogisticRegression(solver="liblinear") depr_message = ( "Starting with mne-python v1.13 'model' default " "will change from LogisticRegression to None. " @@ -398,6 +395,8 @@ def __init__(self, model=None): ) warn(depr_message, FutureWarning) + self.model = model + def __sklearn_tags__(self): """Get sklearn tags.""" tags = super().__sklearn_tags__() diff --git a/mne/decoding/tests/test_base.py b/mne/decoding/tests/test_base.py index de85672036b..bd80e776efa 100644 --- a/mne/decoding/tests/test_base.py +++ b/mne/decoding/tests/test_base.py @@ -277,6 +277,8 @@ def test_get_coef_multiclass(n_features, n_targets): """Test get_coef on multiclass problems.""" # Check patterns with more than 1 regressor X, Y, A = _make_data(n_samples=30000, n_features=n_features, n_targets=n_targets) + with pytest.warns(FutureWarning, match="'model' default"): + _ = LinearModel() lm = LinearModel(LinearRegression()) assert not hasattr(lm, "model_") lm.fit(X, Y) From f138d133a36f9c0e9500a82d6da0922501cf847f Mon Sep 17 00:00:00 2001 From: Genuster Date: Wed, 6 Aug 2025 12:12:00 +0300 Subject: [PATCH 06/20] add short docstring for predict method --- mne/decoding/base.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/mne/decoding/base.py b/mne/decoding/base.py index 7c2740bc347..a17e83d7f02 100644 --- a/mne/decoding/base.py +++ b/mne/decoding/base.py @@ -472,7 +472,13 @@ def fit(self, X, y, **fit_params): return self def predict(self, X): - """...""" + """Predict labels of new data using fitted linear model. + + Parameters + ---------- + X : array, shape (n_samples, n_features) + Data to label. + """ check_is_fitted(self) return self.model_.predict(X) From 838153b91ee2e368a4d668147c164f41e60016a3 Mon Sep 17 00:00:00 2001 From: Genuster Date: Wed, 6 Aug 2025 12:15:31 +0300 Subject: [PATCH 07/20] add returns to the predict docstring --- mne/decoding/base.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mne/decoding/base.py b/mne/decoding/base.py index a17e83d7f02..d0fabab9c36 100644 --- a/mne/decoding/base.py +++ b/mne/decoding/base.py @@ -472,12 +472,17 @@ def fit(self, X, y, **fit_params): return self def predict(self, X): - """Predict labels of new data using fitted linear model. + """Predict class labels for X using fitted linear model. Parameters ---------- X : array, shape (n_samples, n_features) - Data to label. + The data matrix for which we want to get the predictions. + + Returns + ------- + y_pred : array, shape (n_samples,) + Vector containing the class labels for each sample. """ check_is_fitted(self) return self.model_.predict(X) From b7d4c0fe8935e98c14a40ca908485499326db1c4 Mon Sep 17 00:00:00 2001 From: Genuster Date: Wed, 6 Aug 2025 13:19:32 +0300 Subject: [PATCH 08/20] use model.__sklearn__tags__ instead of get_tags --- mne/decoding/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mne/decoding/base.py b/mne/decoding/base.py index d0fabab9c36..f87941af700 100644 --- a/mne/decoding/base.py +++ b/mne/decoding/base.py @@ -22,7 +22,7 @@ from sklearn.linear_model import LogisticRegression from sklearn.metrics import check_scoring from sklearn.model_selection import KFold, StratifiedKFold, check_cv -from sklearn.utils import get_tags, indexable +from sklearn.utils import indexable from sklearn.utils.validation import check_is_fitted, validate_data from ..parallel import parallel_func @@ -401,7 +401,7 @@ def __sklearn_tags__(self): """Get sklearn tags.""" tags = super().__sklearn_tags__() # XXX Change self._orig_model to self.model after 'model' warning cycle - model_tags = get_tags(self._orig_model) + model_tags = self._orig_model.__sklearn_tags__() tags.estimator_type = model_tags.estimator_type if tags.estimator_type is not None: model_type_tags = getattr(model_tags, f"{tags.estimator_type}_tags") From 9f2354f54c3d04959c1a82f48805c4f676c4fccc Mon Sep 17 00:00:00 2001 From: Genuster Date: Wed, 6 Aug 2025 19:16:54 +0300 Subject: [PATCH 09/20] use mne's fix for validate_data --- mne/decoding/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mne/decoding/base.py b/mne/decoding/base.py index f87941af700..5bcb53225ab 100644 --- a/mne/decoding/base.py +++ b/mne/decoding/base.py @@ -23,7 +23,7 @@ from sklearn.metrics import check_scoring from sklearn.model_selection import KFold, StratifiedKFold, check_cv from sklearn.utils import indexable -from sklearn.utils.validation import check_is_fitted, validate_data +from sklearn.utils.validation import check_is_fitted from ..parallel import parallel_func from ..utils import ( @@ -35,6 +35,7 @@ verbose, warn, ) +from ._fixes import validate_data from ._ged import ( _handle_restr_mat, _is_cov_pos_semidef, From 74177d228c8ce27eb5a2081d9f82126fbefee5a9 Mon Sep 17 00:00:00 2001 From: Genuster Date: Wed, 6 Aug 2025 19:57:35 +0300 Subject: [PATCH 10/20] move predict and classes_ back to the wrapped attrs --- mne/decoding/base.py | 27 ++------------------------- mne/decoding/tests/test_base.py | 4 ++++ 2 files changed, 6 insertions(+), 25 deletions(-) diff --git a/mne/decoding/base.py b/mne/decoding/base.py index 5bcb53225ab..c723faced93 100644 --- a/mne/decoding/base.py +++ b/mne/decoding/base.py @@ -378,10 +378,12 @@ class LinearModel(MetaEstimatorMixin, BaseEstimator): _model_attr_wrap = ( "transform", "fit_transform", + "predict", "predict_proba", "predict_log_proba", "decision_function", "score", + "classes_", ) def __init__(self, model=None): @@ -472,22 +474,6 @@ def fit(self, X, y, **fit_params): return self - def predict(self, X): - """Predict class labels for X using fitted linear model. - - Parameters - ---------- - X : array, shape (n_samples, n_features) - The data matrix for which we want to get the predictions. - - Returns - ------- - y_pred : array, shape (n_samples,) - Vector containing the class labels for each sample. - """ - check_is_fitted(self) - return self.model_.predict(X) - @property def filters_(self): check_is_fitted(self) @@ -506,15 +492,6 @@ def filters_(self): filters = filters[0] return filters - @property - def classes_(self): - check_is_fitted(self) - if is_regressor(self.model_): - raise AttributeError("Regressors don't have the 'classes_' attribute") - elif hasattr(self.model_, "classes_"): - return self.model_.classes_ - return None - # XXX Remove this property after 'model' warning cycle @property def model(self): diff --git a/mne/decoding/tests/test_base.py b/mne/decoding/tests/test_base.py index bd80e776efa..aec912211fe 100644 --- a/mne/decoding/tests/test_base.py +++ b/mne/decoding/tests/test_base.py @@ -416,6 +416,10 @@ def test_linearmodel(): wrong_y = rng.rand(n, n_features, 99) clf.fit(X, wrong_y) + clf = LinearModel(StandardScaler()) + with pytest.raises(ValueError, match="classifier or regressor"): + clf.fit(X, Y) + def test_cross_val_multiscore(): """Test cross_val_multiscore for computing scores on decoding over time.""" From 222ed164c9639471f7ad9f5b78cc103f930b62ac Mon Sep 17 00:00:00 2001 From: Genuster Date: Wed, 6 Aug 2025 20:04:48 +0300 Subject: [PATCH 11/20] add fit_transform test --- mne/decoding/tests/test_base.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mne/decoding/tests/test_base.py b/mne/decoding/tests/test_base.py index aec912211fe..6d6072c9923 100644 --- a/mne/decoding/tests/test_base.py +++ b/mne/decoding/tests/test_base.py @@ -28,6 +28,7 @@ is_classifier, is_regressor, ) +from sklearn.discriminant_analysis import LinearDiscriminantAnalysis from sklearn.linear_model import LinearRegression, LogisticRegression, Ridge from sklearn.model_selection import ( GridSearchCV, @@ -380,6 +381,10 @@ def test_linearmodel(): wrong_X = rng.rand(n, n_features, 99) clf.fit(wrong_X, y) + # check fit_transform call + clf = LinearModel(LinearDiscriminantAnalysis()) + _ = clf.fit_transform(X, y) + # check categorical target fit in standard linear model with GridSearchCV parameters = {"kernel": ["linear"], "C": [1, 10]} clf = LinearModel( @@ -416,6 +421,7 @@ def test_linearmodel(): wrong_y = rng.rand(n, n_features, 99) clf.fit(X, wrong_y) + # check that model has to be a predictor clf = LinearModel(StandardScaler()) with pytest.raises(ValueError, match="classifier or regressor"): clf.fit(X, Y) From d7ca9ff7b9c355c08647f48e45a084177705e642 Mon Sep 17 00:00:00 2001 From: Genuster Date: Wed, 6 Aug 2025 20:56:32 +0300 Subject: [PATCH 12/20] __getattr__ can silently catch attribute error from filters_ property --- mne/decoding/base.py | 11 +++++++++-- mne/decoding/tests/test_base.py | 15 ++++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/mne/decoding/base.py b/mne/decoding/base.py index c723faced93..47490c02da8 100644 --- a/mne/decoding/base.py +++ b/mne/decoding/base.py @@ -476,14 +476,16 @@ def fit(self, X, y, **fit_params): @property def filters_(self): - check_is_fitted(self) + check_is_fitted(self.model_) if hasattr(self.model_, "coef_"): # Standard Linear Model filters = self.model_.coef_ elif hasattr(self.model_, "estimators_"): # Linear model with OneVsRestClassifier filters = np.vstack([est.coef_ for est in self.model_.estimators_]) - elif hasattr(self.model_.best_estimator_, "coef_"): + elif hasattr(self.model_, "best_estimator_") and hasattr( + self.model_.best_estimator_, "coef_" + ): # Linear Model with GridSearchCV filters = self.model_.best_estimator_.coef_ else: @@ -511,6 +513,11 @@ def model(self): def model(self, value): self._orig_model = value + # XXX Remove this after 'model' warning cycle + def __repr__(self): + """Avoid FutureWarning from filter_ when printing the instance.""" + return f"LinearModel(model={self._orig_model})" + def _set_cv(cv, estimator=None, X=None, y=None): """Set the default CV depending on whether clf is classifier/regressor.""" diff --git a/mne/decoding/tests/test_base.py b/mne/decoding/tests/test_base.py index 6d6072c9923..e68a1afdcd2 100644 --- a/mne/decoding/tests/test_base.py +++ b/mne/decoding/tests/test_base.py @@ -385,6 +385,16 @@ def test_linearmodel(): clf = LinearModel(LinearDiscriminantAnalysis()) _ = clf.fit_transform(X, y) + # check that model has to have coef_, RBF-SVM doesn't + clf = LinearModel(svm.SVC(kernel="rbf")) + with pytest.raises(ValueError, match="does not have a `coef_`"): + clf.fit(X, y) + + # check that model has to be a predictor + clf = LinearModel(StandardScaler()) + with pytest.raises(ValueError, match="classifier or regressor"): + clf.fit(X, y) + # check categorical target fit in standard linear model with GridSearchCV parameters = {"kernel": ["linear"], "C": [1, 10]} clf = LinearModel( @@ -421,11 +431,6 @@ def test_linearmodel(): wrong_y = rng.rand(n, n_features, 99) clf.fit(X, wrong_y) - # check that model has to be a predictor - clf = LinearModel(StandardScaler()) - with pytest.raises(ValueError, match="classifier or regressor"): - clf.fit(X, Y) - def test_cross_val_multiscore(): """Test cross_val_multiscore for computing scores on decoding over time.""" From 175f6c65ef3509f042a7bee561a311bcc2291acf Mon Sep 17 00:00:00 2001 From: Genuster Date: Thu, 7 Aug 2025 14:34:18 +0300 Subject: [PATCH 13/20] make validation compatible with sklearn<1.6 --- mne/decoding/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mne/decoding/base.py b/mne/decoding/base.py index 47490c02da8..af23805f576 100644 --- a/mne/decoding/base.py +++ b/mne/decoding/base.py @@ -429,8 +429,8 @@ def _fit_transform(self, X, y): return self.fit(X, y).transform(X) def _validate_params(self): - model_type = self.__sklearn_tags__().estimator_type - if model_type not in ("classifier", "regressor"): + is_predictor = is_regressor(self._orig_model) or is_classifier(self._orig_model) + if not is_predictor: raise ValueError( "Linear model should be a supervised predictor " "(classifier or regressor)" From 40dec84456679557951a0c435550d37a5744941e Mon Sep 17 00:00:00 2001 From: Genuster Date: Thu, 7 Aug 2025 20:58:28 +0300 Subject: [PATCH 14/20] more old sklearn fixes --- mne/decoding/base.py | 16 +++++++++++++--- mne/decoding/tests/test_base.py | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/mne/decoding/base.py b/mne/decoding/base.py index af23805f576..5f46e3e641c 100644 --- a/mne/decoding/base.py +++ b/mne/decoding/base.py @@ -381,6 +381,7 @@ class LinearModel(MetaEstimatorMixin, BaseEstimator): "predict", "predict_proba", "predict_log_proba", + "_estimator_type", # remove after sklearn 1.6 "decision_function", "score", "classes_", @@ -428,14 +429,23 @@ def __getattr__(self, attr): def _fit_transform(self, X, y): return self.fit(X, y).transform(X) - def _validate_params(self): - is_predictor = is_regressor(self._orig_model) or is_classifier(self._orig_model) + def _validate_params(self, X): + model = self._orig_model + if isinstance(model, MetaEstimatorMixin): + model = model.estimator + is_predictor = is_regressor(model) or is_classifier(model) if not is_predictor: raise ValueError( "Linear model should be a supervised predictor " "(classifier or regressor)" ) + # For sklearn < 1.6 + try: + self._check_n_features(X, reset=True) + except AttributeError: + pass + def fit(self, X, y, **fit_params): """Estimate the coefficients of the linear model. @@ -456,7 +466,7 @@ def fit(self, X, y, **fit_params): self : instance of LinearModel Returns the modified instance. """ - self._validate_params() + self._validate_params(X) X, y = validate_data(self, X, y, multi_output=True) # fit the Model diff --git a/mne/decoding/tests/test_base.py b/mne/decoding/tests/test_base.py index e68a1afdcd2..628bd10af37 100644 --- a/mne/decoding/tests/test_base.py +++ b/mne/decoding/tests/test_base.py @@ -99,7 +99,7 @@ def test_get_coef(): """Test getting linear coefficients (filters/patterns) from estimators.""" lm_classification = LinearModel(LogisticRegression(solver="liblinear")) assert hasattr(lm_classification, "__sklearn_tags__") - if check_version("sklearn", "1.4"): + if check_version("sklearn", "1.6"): print(lm_classification.__sklearn_tags__()) assert is_classifier(lm_classification.model) assert is_classifier(lm_classification) From 973c8985ba223bc1bc64c1b027d6c3186c1a5fce Mon Sep 17 00:00:00 2001 From: Genuster Date: Fri, 8 Aug 2025 15:45:52 +0300 Subject: [PATCH 15/20] undo fit check from filters property --- mne/decoding/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mne/decoding/base.py b/mne/decoding/base.py index 5f46e3e641c..2b6c2b0adc3 100644 --- a/mne/decoding/base.py +++ b/mne/decoding/base.py @@ -486,7 +486,6 @@ def fit(self, X, y, **fit_params): @property def filters_(self): - check_is_fitted(self.model_) if hasattr(self.model_, "coef_"): # Standard Linear Model filters = self.model_.coef_ From 5e34bd0860b0f0af79518baeeb0e7a0d2640f581 Mon Sep 17 00:00:00 2001 From: Genuster Date: Fri, 8 Aug 2025 20:49:41 +0300 Subject: [PATCH 16/20] add changelog entry --- doc/changes/dev/13361.apichange.rst | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 doc/changes/dev/13361.apichange.rst diff --git a/doc/changes/dev/13361.apichange.rst b/doc/changes/dev/13361.apichange.rst new file mode 100644 index 00000000000..37dec457839 --- /dev/null +++ b/doc/changes/dev/13361.apichange.rst @@ -0,0 +1,7 @@ +Starting with MNE-Python v1.13, ``model`` parameter of :class:`mne.decoding.LinearModel'` +will not be modified, use ``model_`` attribute to access the fitted model. +In addition, the default ``None`` for ``model`` will not automatically set +:class:`sklearn.linear_model.LogisticRegression`, it will need to be provided explicitly. +The ``model`` is expected to be a supervised predictor, i.e. classifier or regressor +(or :class:`sklearn.multiclass.OneVsRestClassifier`), otherwise an error will be raised, +by `Gennadiy Belonosov`_. \ No newline at end of file From 452d98b3d175e0aa980fa77d202bab5457e5b013 Mon Sep 17 00:00:00 2001 From: Genuster Date: Fri, 8 Aug 2025 21:32:54 +0300 Subject: [PATCH 17/20] fix typo in changelog --- doc/changes/dev/13361.apichange.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/dev/13361.apichange.rst b/doc/changes/dev/13361.apichange.rst index 37dec457839..3e46a83146e 100644 --- a/doc/changes/dev/13361.apichange.rst +++ b/doc/changes/dev/13361.apichange.rst @@ -1,4 +1,4 @@ -Starting with MNE-Python v1.13, ``model`` parameter of :class:`mne.decoding.LinearModel'` +Starting with MNE-Python v1.13, ``model`` parameter of :class:`mne.decoding.LinearModel` will not be modified, use ``model_`` attribute to access the fitted model. In addition, the default ``None`` for ``model`` will not automatically set :class:`sklearn.linear_model.LogisticRegression`, it will need to be provided explicitly. From 73357a1675e7c3356e4ca1c0f9a0f7dee5898f6f Mon Sep 17 00:00:00 2001 From: Genuster Date: Mon, 11 Aug 2025 19:10:34 +0300 Subject: [PATCH 18/20] undo api changes --- doc/changes/dev/13361.apichange.rst | 7 --- doc/changes/dev/13361.bugfix.rst | 8 +++ mne/decoding/base.py | 68 +++++++------------------- mne/decoding/tests/test_base.py | 16 +----- mne/decoding/tests/test_transformer.py | 3 +- 5 files changed, 29 insertions(+), 73 deletions(-) delete mode 100644 doc/changes/dev/13361.apichange.rst create mode 100644 doc/changes/dev/13361.bugfix.rst diff --git a/doc/changes/dev/13361.apichange.rst b/doc/changes/dev/13361.apichange.rst deleted file mode 100644 index 3e46a83146e..00000000000 --- a/doc/changes/dev/13361.apichange.rst +++ /dev/null @@ -1,7 +0,0 @@ -Starting with MNE-Python v1.13, ``model`` parameter of :class:`mne.decoding.LinearModel` -will not be modified, use ``model_`` attribute to access the fitted model. -In addition, the default ``None`` for ``model`` will not automatically set -:class:`sklearn.linear_model.LogisticRegression`, it will need to be provided explicitly. -The ``model`` is expected to be a supervised predictor, i.e. classifier or regressor -(or :class:`sklearn.multiclass.OneVsRestClassifier`), otherwise an error will be raised, -by `Gennadiy Belonosov`_. \ No newline at end of file diff --git a/doc/changes/dev/13361.bugfix.rst b/doc/changes/dev/13361.bugfix.rst new file mode 100644 index 00000000000..7038fc21346 --- /dev/null +++ b/doc/changes/dev/13361.bugfix.rst @@ -0,0 +1,8 @@ +``model`` parameter of :class:`mne.decoding.LinearModel` +will not be modified, use ``model_`` attribute to access the fitted model. +To be compatible with all MNE-Python versions you can use +``getattr(clf, "model_", getattr(clf, "model"))`` +The provided ``model`` is expected to be a supervised predictor, +i.e. classifier or regressor (or :class:`sklearn.multiclass.OneVsRestClassifier`), +otherwise an error will be raised. +by `Gennadiy Belonosov`_. \ No newline at end of file diff --git a/mne/decoding/base.py b/mne/decoding/base.py index 2b6c2b0adc3..adae374ea25 100644 --- a/mne/decoding/base.py +++ b/mne/decoding/base.py @@ -388,24 +388,13 @@ class LinearModel(MetaEstimatorMixin, BaseEstimator): ) def __init__(self, model=None): - # XXX Remove the clause after warning cycle - if model is None: - model = LogisticRegression(solver="liblinear") - depr_message = ( - "Starting with mne-python v1.13 'model' default " - "will change from LogisticRegression to None. " - "From now on please set model=LogisticRegression" - "(solver='liblinear') explicitly." - ) - warn(depr_message, FutureWarning) - self.model = model def __sklearn_tags__(self): """Get sklearn tags.""" tags = super().__sklearn_tags__() - # XXX Change self._orig_model to self.model after 'model' warning cycle - model_tags = self._orig_model.__sklearn_tags__() + model = self.model if self.model is not None else LogisticRegression() + model_tags = model.__sklearn_tags__() tags.estimator_type = model_tags.estimator_type if tags.estimator_type is not None: model_type_tags = getattr(model_tags, f"{tags.estimator_type}_tags") @@ -415,8 +404,7 @@ def __sklearn_tags__(self): def __getattr__(self, attr): """Wrap to model for some attributes.""" if attr in LinearModel._model_attr_wrap: - # XXX Change self._orig_model to self.model after 'model' warning cycle - model = self.model_ if "model_" in self.__dict__ else self._orig_model + model = self.model_ if "model_" in self.__dict__ else self.model if attr == "fit_transform" and hasattr(model, "fit_transform"): return self._fit_transform else: @@ -430,15 +418,16 @@ def _fit_transform(self, X, y): return self.fit(X, y).transform(X) def _validate_params(self, X): - model = self._orig_model - if isinstance(model, MetaEstimatorMixin): - model = model.estimator - is_predictor = is_regressor(model) or is_classifier(model) - if not is_predictor: - raise ValueError( - "Linear model should be a supervised predictor " - "(classifier or regressor)" - ) + if self.model is not None: + model = self.model + if isinstance(model, MetaEstimatorMixin): + model = model.estimator + is_predictor = is_regressor(model) or is_classifier(model) + if not is_predictor: + raise ValueError( + "Linear model should be a supervised predictor " + "(classifier or regressor)" + ) # For sklearn < 1.6 try: @@ -470,8 +459,11 @@ def fit(self, X, y, **fit_params): X, y = validate_data(self, X, y, multi_output=True) # fit the Model - # XXX Change self._orig_model to self.model after 'model' warning cycle - self.model_ = clone(self._orig_model) + self.model_ = ( + clone(self.model) + if self.model is not None + else LogisticRegression(solver="liblinear") + ) self.model_.fit(X, y, **fit_params) # Computes patterns using Haufe's trick: A = Cov_X . W . Precision_Y @@ -503,30 +495,6 @@ def filters_(self): filters = filters[0] return filters - # XXX Remove this property after 'model' warning cycle - @property - def model(self): - if "model_" in self.__dict__: - depr_message = ( - "Starting with mne-python v1.13 'model' attribute " - "of LinearModel will not be fitted, " - "please use 'model_' instead" - ) - warn(depr_message, FutureWarning) - return self.model_ - else: - return self._orig_model - - # XXX Remove this after 'model' warning cycle - @model.setter - def model(self, value): - self._orig_model = value - - # XXX Remove this after 'model' warning cycle - def __repr__(self): - """Avoid FutureWarning from filter_ when printing the instance.""" - return f"LinearModel(model={self._orig_model})" - def _set_cv(cv, estimator=None, X=None, y=None): """Set the default CV depending on whether clf is classifier/regressor.""" diff --git a/mne/decoding/tests/test_base.py b/mne/decoding/tests/test_base.py index 628bd10af37..a41b3246ed2 100644 --- a/mne/decoding/tests/test_base.py +++ b/mne/decoding/tests/test_base.py @@ -278,13 +278,10 @@ def test_get_coef_multiclass(n_features, n_targets): """Test get_coef on multiclass problems.""" # Check patterns with more than 1 regressor X, Y, A = _make_data(n_samples=30000, n_features=n_features, n_targets=n_targets) - with pytest.warns(FutureWarning, match="'model' default"): - _ = LinearModel() lm = LinearModel(LinearRegression()) assert not hasattr(lm, "model_") lm.fit(X, Y) - with pytest.warns(FutureWarning, match="'model' attribute of LinearModel"): - assert lm.model is lm.model_ + assert lm.model is not lm.model_ assert_array_equal(lm.filters_.shape, lm.patterns_.shape) if n_targets == 1: want_shape = (n_features,) @@ -370,7 +367,7 @@ def test_linearmodel(): """Test LinearModel class for computing filters and patterns.""" # check categorical target fit in standard linear model rng = np.random.RandomState(0) - clf = LinearModel(LogisticRegression(solver="liblinear")) + clf = LinearModel() n, n_features = 20, 3 X = rng.rand(n, n_features) y = np.arange(n) % 2 @@ -491,16 +488,7 @@ def test_cross_val_multiscore(): assert_array_equal(manual, auto) -# XXX Remove the filterwarning after 'model' warning cycle -@pytest.mark.filterwarnings("ignore::FutureWarning") @parametrize_with_checks([LinearModel(LogisticRegression())]) def test_sklearn_compliance(estimator, check): """Test LinearModel compliance with sklearn.""" - # XXX Remove the ignores after 'model' warning cycle - ignores = ( - "check_estimators_overwrite_params", - "check_parameters_default_constructible", - ) - if any(ignore in str(check) for ignore in ignores): - return check(estimator) diff --git a/mne/decoding/tests/test_transformer.py b/mne/decoding/tests/test_transformer.py index 6df3405c297..06f0c157814 100644 --- a/mne/decoding/tests/test_transformer.py +++ b/mne/decoding/tests/test_transformer.py @@ -17,7 +17,6 @@ from sklearn.decomposition import PCA from sklearn.kernel_ridge import KernelRidge -from sklearn.linear_model import LogisticRegression from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler from sklearn.utils.estimator_checks import parametrize_with_checks @@ -233,7 +232,7 @@ def test_vectorizer(): clf = make_pipeline( Vectorizer(), StandardScaler(), - LinearModel(LogisticRegression(solver="liblinear")), + LinearModel(), ) clf.fit(X_arr, y) From dd68588bcb528a589fb874d60e1a0ac27ed1a9e0 Mon Sep 17 00:00:00 2001 From: Genuster Date: Mon, 11 Aug 2025 19:15:45 +0300 Subject: [PATCH 19/20] another tiny undo --- mne/decoding/tests/test_transformer.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/mne/decoding/tests/test_transformer.py b/mne/decoding/tests/test_transformer.py index 06f0c157814..b6d97d3e435 100644 --- a/mne/decoding/tests/test_transformer.py +++ b/mne/decoding/tests/test_transformer.py @@ -229,11 +229,7 @@ def test_vectorizer(): # And that pipelines work properly X_arr = EpochsArray(X, create_info(12, 1000.0, "eeg")) vect.fit(X_arr) - clf = make_pipeline( - Vectorizer(), - StandardScaler(), - LinearModel(), - ) + clf = make_pipeline(Vectorizer(), StandardScaler(), LinearModel()) clf.fit(X_arr, y) From a0883323eecf12b058be063167096771b1422500 Mon Sep 17 00:00:00 2001 From: Genuster Date: Mon, 18 Aug 2025 16:30:29 +0300 Subject: [PATCH 20/20] TST: All examples [circle full]