diff --git a/doc/whats_new/v1.1.rst b/doc/whats_new/v1.1.rst index b9651a1e1b6f8..329f87813a389 100644 --- a/doc/whats_new/v1.1.rst +++ b/doc/whats_new/v1.1.rst @@ -187,6 +187,11 @@ Changelog multilabel classification. :pr:`19689` by :user:`Guillaume Lemaitre `. +- |Enhancement| :class:`linear_model.RidgeCV` and + :class:`linear_model.RidgeClassifierCV` now raise consistent error message + when passed invalid values for `alphas`. + :pr:`21606` by :user:`Arturo Amor `. + - |Enhancement| :class:`linear_model.Ridge` and :class:`linear_model.RidgeClassifier` now raise consistent error message when passed invalid values for `alpha`, `max_iter` and `tol`. diff --git a/sklearn/linear_model/_ridge.py b/sklearn/linear_model/_ridge.py index 09ef46ffc7ebe..1426201a893aa 100644 --- a/sklearn/linear_model/_ridge.py +++ b/sklearn/linear_model/_ridge.py @@ -10,6 +10,7 @@ from abc import ABCMeta, abstractmethod +from functools import partial import warnings import numpy as np @@ -1864,12 +1865,6 @@ def fit(self, X, y, sample_weight=None): self.alphas = np.asarray(self.alphas) - if np.any(self.alphas <= 0): - raise ValueError( - "alphas must be strictly positive. Got {} containing some " - "negative or null value instead.".format(self.alphas) - ) - X, y, X_offset, y_offset, X_scale = LinearModel._preprocess_data( X, y, @@ -2038,9 +2033,30 @@ def fit(self, X, y, sample_weight=None): the validation score. """ cv = self.cv + + check_scalar_alpha = partial( + check_scalar, + target_type=numbers.Real, + min_val=0.0, + include_boundaries="neither", + ) + + if isinstance(self.alphas, (np.ndarray, list, tuple)): + n_alphas = 1 if np.ndim(self.alphas) == 0 else len(self.alphas) + if n_alphas != 1: + for index, alpha in enumerate(self.alphas): + alpha = check_scalar_alpha(alpha, f"alphas[{index}]") + else: + self.alphas[0] = check_scalar_alpha(self.alphas[0], "alphas") + else: + # check for single non-iterable item + self.alphas = check_scalar_alpha(self.alphas, "alphas") + + alphas = np.asarray(self.alphas) + if cv is None: estimator = _RidgeGCV( - self.alphas, + alphas, fit_intercept=self.fit_intercept, normalize=self.normalize, scoring=self.scoring, @@ -2059,7 +2075,8 @@ def fit(self, X, y, sample_weight=None): raise ValueError("cv!=None and store_cv_values=True are incompatible") if self.alpha_per_target: raise ValueError("cv!=None and alpha_per_target=True are incompatible") - parameters = {"alpha": self.alphas} + + parameters = {"alpha": alphas} solver = "sparse_cg" if sparse.issparse(X) else "auto" model = RidgeClassifier if is_classifier(self) else Ridge gs = GridSearchCV( diff --git a/sklearn/linear_model/tests/test_ridge.py b/sklearn/linear_model/tests/test_ridge.py index 1160e2db57fc6..975d16df06f12 100644 --- a/sklearn/linear_model/tests/test_ridge.py +++ b/sklearn/linear_model/tests/test_ridge.py @@ -1270,19 +1270,51 @@ def test_ridgecv_int_alphas(): ridge.fit(X, y) -def test_ridgecv_negative_alphas(): - X = np.array([[-1.0, -1.0], [-1.0, 0], [-0.8, -1.0], [1.0, 1.0], [1.0, 0.0]]) - y = [1, 1, 1, -1, -1] +@pytest.mark.parametrize("Estimator", [RidgeCV, RidgeClassifierCV]) +@pytest.mark.parametrize( + "params, err_type, err_msg", + [ + ({"alphas": (1, -1, -100)}, ValueError, r"alphas\[1\] == -1, must be > 0.0"), + ( + {"alphas": (-0.1, -1.0, -10.0)}, + ValueError, + r"alphas\[0\] == -0.1, must be > 0.0", + ), + ( + {"alphas": (1, 1.0, "1")}, + TypeError, + r"alphas\[2\] must be an instance of , not ", + ), + ], +) +def test_ridgecv_alphas_validation(Estimator, params, err_type, err_msg): + """Check the `alphas` validation in RidgeCV and RidgeClassifierCV.""" - # Negative integers - ridge = RidgeCV(alphas=(-1, -10, -100)) - with pytest.raises(ValueError, match="alphas must be strictly positive"): - ridge.fit(X, y) + n_samples, n_features = 5, 5 + X = rng.randn(n_samples, n_features) + y = rng.randint(0, 2, n_samples) - # Negative floats - ridge = RidgeCV(alphas=(-0.1, -1.0, -10.0)) - with pytest.raises(ValueError, match="alphas must be strictly positive"): - ridge.fit(X, y) + with pytest.raises(err_type, match=err_msg): + Estimator(**params).fit(X, y) + + +@pytest.mark.parametrize("Estimator", [RidgeCV, RidgeClassifierCV]) +def test_ridgecv_alphas_scalar(Estimator): + """Check the case when `alphas` is a scalar. + This case was supported in the past when `alphas` where converted + into array in `__init__`. + We add this test to ensure backward compatibility. + """ + + n_samples, n_features = 5, 5 + X = rng.randn(n_samples, n_features) + if Estimator is RidgeCV: + y = rng.randn(n_samples) + else: + y = rng.randint(0, 2, n_samples) + + Estimator(alphas=1).fit(X, y) def test_raises_value_error_if_solver_not_supported():