FIX `predict` to also use `multiclass_colors` in DecisionBoundaryDisplay

<!--
🙌 Thanks for contributing a pull request!

👀 Please ensure you have taken a look at the contribution guidelines:
https://github.com/[USER_REDACTED]n/scikit-learn/blob/main/CONTRIBUTING.md

✅ In particular following the pull request checklist will increase the likelihood
of having maintainers review your PR:
https://scikit-learn.org/dev/developers/contributing.html#pull-request-checklist

📋 If your PR is likely to affect users, you will need to add a changelog entry
describing your PR changes, see:
https://github.com/[USER_REDACTED]n/scikit-learn/blob/main/doc/whats_new/upcoming_changes/README.md
-->

#### [NAME_REDACTED]/PRs
<!--
Example: Fixes #1234. See also #3456.
Please use keywords (e.g., Fixes) to create link to the issues or pull requests
you resolved, so that they will automatically be closed when your pull request
is merged. See https://github.com/blog/1506-closing-issues-via-pull-requests
-->
Fixes #32872 ~and the `predict` part of #32866~

#### What does this implement/fix? Explain your changes.
Previously, the case distinction for binary vs. multiclass was made based on the response dimension (see #32918 for reference), which led to multiclass `predict` ignoring the `multiclass_colors` parameter.
The colors are now selected based on the `n_classes` display class variable, which is extracted in `from_estimator`.

Note that a distinction based on the response shape still has to be made for plotting. ~in two cases (`contour` and `pcolormesh`). I'm not sure if there is a more elegant solution, but I didn't find a way to handle `pcolormesh` in the same way for `predict` as in the other cases.~

I adapted the docstrings and the exmple and added the response functions to the tests (and adapted them accordingly).

EDIT: The output of the code in issue #32872 now looks like this (Note that the levels problem from #32866 is still there for `predict`, but this PR is only concerned with making it use the same color map as the other methods, which it does now):
<img width="1211" height="1211" alt="image" src="https://github.com/[USER_REDACTED]nts/assets/45e4d2b5-547c-41a8-a61c-6cd2377c9963" />

~Since every class is plotted separately now (as it was already for response method `predict_proba` and `decision_function`), this also fixes the `levels` issue noted in #32866. However, there are white lines between the classes for `predict` with `countourf`. If this is a problem (and using pcolormesh is not an option for any reason), the current approach will need to be adapted further.~

#### AI usage disclosure
<!--
If AI tools were involved in creating this PR, please check all boxes that apply
below and make sure that you adhere to our [NAME_REDACTED]:
https://scikit-learn.org/dev/developers/contributing.html#automated-contributions-policy
-->
I used AI assistance for:
- [x] Code generation (e.g., when writing an implementation or fixing a bug)
- [ ] Test/benchmark generation
- [ ] Documentation (including examples)
- [x] Research and understanding

#### Any other comments?
I also added a test for the default value for `multiclass_colors`, which was missing.

<!--
Thank you for your patience. Changes to scikit-learn require careful
attention, but with limited maintainer time, not every contribution can be reviewed
quickly.
For more information and tips on improving your pull request, see:
https://scikit-learn.org/dev/faq.html#why-is-my-pull-request-not-getting-any-attention.

Thanks for contributing!
-->

@@ -0,0 +1,3 @@
+- In :class:`inspection.DecisionBoundaryDisplay`, `multiclass_colors` is now also used
+  for multiclass plotting when `response_method="predict"`.
+  By :user:`[NAME_REDACTED] <AnneBeyer>`.

@@ -5,7 +5,7 @@

 import numpy as np

-from sklearn.base import is_regressor
+from sklearn.base import is_classifier, is_clusterer, is_outlier_detector, is_regressor
 from sklearn.preprocessing import LabelEncoder
 from sklearn.utils import _safe_indexing
 from sklearn.utils._dataframe import is_pandas_df, is_polars_df
@@ -83,27 +83,46 @@ class DecisionBoundaryDisplay:
     xx1 : ndarray of shape (grid_resolution, grid_resolution)
         Second output of :func:`meshgrid <numpy.meshgrid>`.

+    n_classes : int
+        Expected number of unique classes or labels if `response` was generated by a
+        :term:`classifier` or a :term:`clusterer`.
+
+        For :term:`outlier detectors`, `n_classes` should be set to 2 by definition
+        (inlier or outlier).
+
+        For :term:`regressors`, `n_classes` should also be set to 2 by convention
+        (continuous responses are displayed the same way as unthresholded binary
+        responses).
+
+        .. versionadded:: 1.9
+
     response : ndarray of shape (grid_resolution, grid_resolution) or \
             (grid_resolution, grid_resolution, n_classes)
         Values of the response function.

     multiclass_colors : list of str or str, default=None
-        Specifies how to color each class when plotting all classes of multiclass
-        problem. Ignored for binary problems and multiclass problems when plotting a
-        single prediction value per point.
+        Specifies how to color each class when plotting all classes of
+        :term:`multiclass` problems.
+
         Possible inputs are:

         * list: list of Matplotlib
           `color <https://matplotlib.org/stable/users/explain/colors/colors.html#colors-def>`_
           strings, of length `n_classes`
         * str: name of :class:`matplotlib.colors.Colormap`
-        * None: 'viridis' colormap is used to sample colors
+        * None: 'tab10' colormap is used to sample colors if the number of
+          classes is less than or equal to 10, otherwise 'gist_rainbow' colormap.
+
+        Single color (fading to white) colormaps will be generated from the colors in
+        the list or colors taken from the colormap, and passed to the `cmap` parameter
+        of the `plot_method`.

-        Single color colormaps will be generated from the colors in the list or
-        colors taken from the colormap and passed to the `cmap` parameter of
-        the `plot_method`.
+        For :term:`binary` problems, this is ignored and `cmap` or `colors` can be
+        passed as kwargs instead, otherwise, the default colormap ('viridis') is used.

         .. versionadded:: 1.7
+        .. versionchanged:: 1.9
+           `multiclass_colors` is now also used when `response_method="predict"`

     xlabel : str, default=None
         Default label to place on x axis.
@@ -121,7 +140,7 @@ class DecisionBoundaryDisplay:

     multiclass_colors_ : array of shape (n_classes, 4)
         Colors used to plot each class in multiclass problems.
-        Only defined when `color_of_interest` is None.
+        Only defined when `n_classes` > 2.

         .. versionadded:: 1.7

@@ -137,6 +156,7 @@ class DecisionBoundaryDisplay:

     Examples
     --------
+    >>> import matplotlib as mpl
     >>> import matplotlib.pyplot as plt
     >>> import numpy as np
     >>> from sklearn.datasets import load_iris
@@ -151,22 +171,35 @@ class DecisionBoundaryDisplay:
     >>> tree = DecisionTreeClassifier().fit(iris.data[:, :2], iris.target)
     >>> y_pred = np.reshape(tree.predict(grid), feature_1.shape)
     >>> display = DecisionBoundaryDisplay(
-    ...     xx0=feature_1, xx1=feature_2, response=y_pred
+    ...     xx0=feature_1, xx1=feature_2, n_classes=len(tree.classes_), response=y_pred
     ... )
     >>> display.plot()
     <...>
     >>> display.ax_.scatter(
-    ...     iris.data[:, 0], iris.data[:, 1], c=iris.target, edgecolor="black"
+    ...     iris.data[:, 0],
+    ...     iris.data[:, 1],
+    ...     c=iris.target,
+    ...     cmap=mpl.colors.ListedColormap(display.multiclass_colors_),
+    ...     edgecolor="black"
     ... )
     <...>
     >>> plt.show()
     """

     def __init__(
-        self, *, xx0, xx1, response, multiclass_colors=None, xlabel=None, ylabel=None
+        self,
+        *,
+        xx0,
+        xx1,
+        n_classes,
+        response,
+        multiclass_colors=None,
+        xlabel=None,
+        ylabel=None,
     ):
         self.xx0 = xx0
         self.xx1 = xx1
+        self.n_classes = n_classes
         self.response = response
         self.multiclass_colors = multiclass_colors
         self.xlabel = xlabel
@@ -195,7 +228,9 @@ def plot(self, plot_method="contourf", ax=None, xlabel=None, ylabel=None, **kwar
             Overwrite the y-axis label.

         **kwargs : dict
-            Additional keyword arguments to be passed to the `plot_method`.
+            Additional keyword arguments to be passed to the `plot_method`. For
+            :term:`binary` problems, `cmap` or `colors` can be set here to specify the
+            colormap or colors, otherwise the default colormap ('viridis') is used.

         Returns
         -------
@@ -216,39 +251,37 @@ def plot(self, plot_method="contourf", ax=None, xlabel=None, ylabel=None, **kwar
             _, ax = plt.subplots()

         plot_func = getattr(ax, plot_method)
-        if self.response.ndim == 2:
+        if self.n_classes == 2:
             self.surface_ = plot_func(self.xx0, self.xx1, self.response, **kwargs)
-        else:  # self.response.ndim == 3
-            n_responses = self.response.shape[-1]
+        else:  # multiclass
             for kwarg in ("cmap", "colors"):
                 if kwarg in kwargs:
                     warnings.warn(
                         f"'{kwarg}' is ignored in favor of 'multiclass_colors' "
-                        "in the multiclass case when the response method is "
-                        "'decision_function' or 'predict_proba'."
+                        "in the multiclass case."
                     )
                     del kwargs[kwarg]

             if self.multiclass_colors is None or isinstance(
                 self.multiclass_colors, str
             ):
                 if self.multiclass_colors is None:
-                    cmap = "tab10" if n_responses <= 10 else "gist_rainbow"
+                    cmap = "tab10" if self.n_classes <= 10 else "gist_rainbow"
                 else:
                     cmap = self.multiclass_colors

                 # Special case for the tab10 and tab20 colormaps that encode a
                 # discrete set of colors that are easily distinguishable
                 # contrary to other colormaps that are continuous.
-                if cmap == "tab10" and n_responses <= 10:
-                    colors = plt.get_cmap("tab10", 10).colors[:n_responses]
-                elif cmap == "tab20" and n_responses <= 20:
-                    colors = plt.get_cmap("tab20", 20).colors[:n_responses]
+                if cmap == "tab10" and self.n_classes <= 10:
+                    colors = plt.get_cmap("tab10", 10).colors[: self.n_classes]
+                elif cmap == "tab20" and self.n_classes <= 20:
+                    colors = plt.get_cmap("tab20", 20).colors[: self.n_classes]
                 else:
-                    cmap = plt.get_cmap(cmap, n_responses)
+                    cmap = plt.get_cmap(cmap, self.n_classes)
                     if not hasattr(cmap, "colors"):
-                        # For LinearSegmentedColormap
-                        colors = cmap(np.linspace(0, 1, n_responses))
+                        # Get `LinearSegmentedColormap` for non-qualitative cmaps
+                        colors = cmap(np.linspace(0, 1, self.n_classes))
                     else:
                         colors = cmap.colors
             elif isinstance(self.multiclass_colors, list):
@@ -257,25 +290,37 @@ def plot(self, plot_method="contourf", ax=None, xlabel=None, ylabel=None, **kwar
                 raise ValueError("'multiclass_colors' must be a list or a str.")

             self.multiclass_colors_ = colors
-            if plot_method == "contour":
-                # Plot only argmax map for contour
-                class_map = self.response.argmax(axis=2)
+
+            if self.response.ndim == 2:  # predict
+                # `pcolormesh` requires cmap, for the others it makes no difference
+                cmap = mpl.colors.ListedColormap(colors)
                 self.surface_ = plot_func(
-                    self.xx0, self.xx1, class_map, colors=colors, **kwargs
+                    self.xx0, self.xx1, self.response, cmap=cmap, **kwargs
+                )
+
+            # predict_proba and decision_function differ for plotting methods
+            elif plot_method == "contour":
+                # Plot only integer class values
+                self.surface_ = plot_func(
+                    self.xx0,
+                    self.xx1,
+                    self.response.argmax(axis=2),
+                    colors=colors,
+                    **kwargs,
                 )
             else:
                 multiclass_cmaps = [
                     mpl.colors.LinearSegmentedColormap.from_list(
-                        f"colormap_{class_idx}", [(1.0, 1.0, 1.0, 1.0), (r, g, b, 1.0)]
+                        f"colormap_{class_idx}",
+                        [(1.0, 1.0, 1.0, 1.0), (r, g, b, 1.0)],
                     )
                     for class_idx, (r, g, b, _) in enumerate(colors)
                 ]
-
                 self.surface_ = []
                 for class_idx, cmap in enumerate(multiclass_cmaps):
                     response = np.ma.array(
                         self.response[:, :, class_idx],
-                        mask=~(self.response.argmax(axis=2) == class_idx),
+                        mask=(self.response.argmax(axis=2) != class_idx),
                     )
                     self.surface_.append(
                         plot_func(self.xx0, self.xx1, response, cmap=cmap, **kwargs)
@@ -348,20 +393,19 @@ def from_estimator(
                 For multiclass problems, 'auto' no longer defaults to 'predict'.

         class_of_interest : int, float, bool or str, default=None
-            The class to be plotted when `response_method` is 'predict_proba'
-            or 'decision_function'. If None, `estimator.classes_[1]` is considered
-            the positive class for binary classifiers. For multiclass
-            classifiers, if None, all classes will be represented in the
-            decision boundary plot; the class with the highest response value
+            The class to be plotted. For :term:`binary` classifiers, if None,
+            `estimator.classes_[1]` is considered the positive class. For
+            :term:`multiclass` classifiers, if None, all classes will be represented in
+            the decision boundary plot; when `response_method` is :term:`predict_proba`
+            or :term:`decision_function`, the class with the highest response value
             at each point is plotted. The color of each class can be set via
             `multiclass_colors`.

             .. versionadded:: 1.4

         multiclass_colors : list of str, or str, default=None
-            Specifies how to color each class when plotting multiclass
-            'predict_proba' or 'decision_function' and `class_of_interest` is
-            None. Ignored in all other cases.
+            Specifies how to color each class when plotting :term:`multiclass` problems
+            and `class_of_interest` is None.

             Possible inputs are:

@@ -373,11 +417,17 @@ def from_estimator(
                 classes is less than or equal to 10, otherwise 'gist_rainbow'
                 colormap.

-            Single color colormaps will be generated from the colors in the list or
-            colors taken from the colormap, and passed to the `cmap` parameter of
-            the `plot_method`.
+            Single color (fading to white) colormaps will be generated from the colors
+            in the list or colors taken from the colormap, and passed to the `cmap`
+            parameter of the `plot_method`.
+
+            For :term:`binary` problems, this is ignored and `cmap` or `colors` can be
+            passed as kwargs instead, otherwise, the default colormap ('viridis') is
+            used.

             .. versionadded:: 1.7
+            .. versionchanged:: 1.9
+               `multiclass_colors` is now also used when `response_method="predict"`

         xlabel : str, default=None
             The label used for the x-axis. If `None`, an attempt is made to
@@ -394,8 +444,7 @@ def from_estimator(
             created.

         **kwargs : dict
-            Additional keyword arguments to be passed to the
-            `plot_method`.
+            Additional keyword arguments to be passed to the `plot_method`.

         Returns
         -------
@@ -412,6 +461,7 @@ def from_estimator(

         Examples
         --------
+        >>> import matplotlib as mpl
         >>> import matplotlib.pyplot as plt
         >>> from sklearn.datasets import load_iris
         >>> from sklearn.linear_model import LogisticRegression
@@ -424,7 +474,8 @@ def from_estimator(
         ...     xlabel=iris.feature_names[0], ylabel=iris.feature_names[1],
         ...     alpha=0.5,
         ... )
-        >>> disp.ax_.scatter(X[:, 0], X[:, 1], c=iris.target, edgecolor="k")
+        >>> cmap = mpl.colors.ListedColormap(disp.multiclass_colors_)
+        >>> disp.ax_.scatter(X[:, 0], X[:, 1], c=iris.target, edgecolor="k", cmap=cmap)
         <...>
         >>> plt.show()
         """
@@ -520,6 +571,8 @@ def from_estimator(
                 # to our user when interacting with
                 # `DecisionBoundaryDisplay.from_estimator`
                 raise ValueError(
+                    # Note: it is ok to use estimator.classes_ here, as this error will
+                    # only be thrown if estimator is a classifier
                     f"class_of_interest={class_of_interest} is not a valid label: It "
                     f"should be one of {estimator.classes_}"
                 ) from exc
@@ -546,6 +599,17 @@ def from_estimator(
             else:
                 response = response.reshape(*xx0.shape, response.shape[-1])

+        if (
+            class_of_interest is not None
+            or is_regressor(estimator)
+            or is_outlier_detector(estimator)
+        ):
+            n_classes = 2
+        elif is_classifier(estimator):
+            n_classes = len(estimator.classes_)
+        elif is_clusterer(estimator):
+            n_classes = len(np.unique(estimator.labels_))
+
         if xlabel is None:
             xlabel = X.columns[0] if hasattr(X, "columns") else ""

@@ -555,6 +619,7 @@ def from_estimator(
         display = cls(
             xx0=xx0,
             xx1=xx1,
+            n_classes=n_classes,
             response=response,
             multiclass_colors=multiclass_colors,
             xlabel=xlabel,

@@ -3,10 +3,15 @@
 import numpy as np
 import pytest

-from sklearn.base import BaseEstimator, ClassifierMixin
+from sklearn.base import (
+    BaseEstimator,
+    ClassifierMixin,
+)
+from sklearn.cluster import KMeans
 from sklearn.datasets import (
     load_diabetes,
     load_iris,
+    make_blobs,
     make_classification,
     make_multilabel_classification,
 )
@@ -239,7 +244,7 @@ def test_decision_boundary_display_classifier(

 [USER_REDACTED].mark.parametrize("response_method", ["auto", "predict", "decision_function"])
-[USER_REDACTED].mark.parametrize("plot_method", ["contourf", "contour"])
+[USER_REDACTED].mark.parametrize("plot_method", ["contourf", "contour", "pcolormesh"])
 def test_decision_boundary_display_outlier_detector(
     pyplot, response_method, plot_method
 ):
@@ -256,7 +261,10 @@ def test_decision_boundary_display_outlier_detector(
         eps=eps,
         ax=ax,
     )
-    assert isinstance(disp.surface_, pyplot.matplotlib.contour.QuadContourSet)
+    if plot_method == "pcolormesh":
+        assert isinstance(disp.surface_, pyplot.matplotlib.collections.QuadMesh)
+    else:
+        assert isinstance(disp.surface_, pyplot.matplotlib.contour.QuadContourSet)
     assert disp.ax_ == ax
     assert disp.figure_ == fig

@@ -288,7 +296,12 @@ def test_decision_boundary_display_regressor(pyplot, response_method, plot_metho
         eps=eps,
         plot_method=plot_method,
     )
-    assert isinstance(disp.surface_, pyplot.matplotlib.contour.QuadContourSet)
+    if disp.n_classes == 2 or plot_method == "contour":
+        assert isinstance(disp.surface_, pyplot.matplotlib.contour.QuadContourSet)
+    else:
+        assert isinstance(disp.surface_, list)
+        for surface in disp.surface_:
+            assert isinstance(surface, pyplot.matplotlib.contour.QuadContourSet)
     assert disp.ax_ == ax
     assert disp.figure_ == fig

@@ -618,15 +631,26 @@ def test_multiclass_plot_max_class(pyplot, response_method):

 [USER_REDACTED].mark.parametrize(
-    "multiclass_colors",
+    "multiclass_colors, n_classes",
     [
-        "plasma",
-        "Blues",
-        ["red", "green", "blue"],
+        (None, 3),
+        (None, 11),
+        ("plasma", 3),
+        ("Blues", 3),
+        (["red", "green", "blue"], 3),
     ],
 )
+[USER_REDACTED].mark.parametrize(
+    "response_method", ["decision_function", "predict_proba", "predict"]
+)
 [USER_REDACTED].mark.parametrize("plot_method", ["contourf", "contour", "pcolormesh"])
-def test_multiclass_colors_cmap(pyplot, plot_method, multiclass_colors):
+def test_multiclass_colors_cmap(
+    pyplot,
+    n_classes,
+    response_method,
+    plot_method,
+    multiclass_colors,
+):
     """Check correct cmap used for all `multiclass_colors` inputs."""
     import matplotlib as mpl

@@ -635,36 +659,84 @@ def test_multiclass_colors_cmap(pyplot, plot_method, multiclass_colors):
             "Matplotlib >= 3.5 is needed for `==` to check equivalence of colormaps"
         )

-    X, y = load_iris_2d_scaled()
+    X, y = make_blobs(n_samples=150, centers=n_classes, n_features=2, random_state=42)
     clf = LogisticRegression().fit(X, y)

     disp = DecisionBoundaryDisplay.from_estimator(
         clf,
         X,
+        response_method=response_method,
         plot_method=plot_method,
         multiclass_colors=multiclass_colors,
     )

-    if multiclass_colors == "plasma":
+    if multiclass_colors is None:
+        if len(clf.classes_) <= 10:
+            colors = mpl.pyplot.get_cmap("tab10", 10).colors[: len(clf.classes_)]
+        else:
+            cmap = mpl.pyplot.get_cmap("gist_rainbow", len(clf.classes_))
+            colors = cmap(np.linspace(0, 1, len(clf.classes_)))
+    elif multiclass_colors == "plasma":
         colors = mpl.pyplot.get_cmap(multiclass_colors, len(clf.classes_)).colors
     elif multiclass_colors == "Blues":
         cmap = mpl.pyplot.get_cmap(multiclass_colors, len(clf.classes_))
         colors = cmap(np.linspace(0, 1, len(clf.classes_)))
     else:
         colors = [mpl.colors.to_rgba(color) for color in multiclass_colors]

-    if plot_method != "contour":
+    # Make sure the colormap has enough distinct colors.
+    assert disp.n_classes == len(np.unique(colors, axis=0))
+
+    if response_method == "predict":
+        cmap = mpl.colors.ListedColormap(colors)
+        assert disp.surface_.cmap == cmap
+    elif plot_method != "contour":
         cmaps = [
             mpl.colors.LinearSegmentedColormap.from_list(
                 f"colormap_{class_idx}", [(1.0, 1.0, 1.0, 1.0), (r, g, b, 1.0)]
             )
             for class_idx, (r, g, b, _) in enumerate(colors)
         ]
+        # Make sure every class has its own surface.
+        assert len(disp.surface_) == disp.n_classes
+
         for idx, quad in enumerate(disp.surface_):
             assert quad.cmap == cmaps[idx]
     else:
         assert_allclose(disp.surface_.colors, colors)

+    # non-regression test for issue #32866 (currently still fails)
+    # if hasattr(disp.surface_, "levels"):
+    #    assert len(disp.surface_.levels) >= disp.n_classes
+
+
+[USER_REDACTED].mark.parametrize(
+    "estimator, n_blobs, expected_n_classes",
+    [
+        (DecisionTreeClassifier(random_state=0), 7, 7),
+        (DecisionTreeClassifier(random_state=0), 2, 2),
+        (KMeans(n_clusters=7, random_state=0), 7, 7),
+        (KMeans(n_clusters=2, random_state=0), 2, 2),
+        (DecisionTreeRegressor(random_state=0), 7, 2),
+        (IsolationForest(random_state=0), 7, 2),
+    ],
+)
+def test_n_classes_attribute(pyplot, estimator, n_blobs, expected_n_classes):
+    """Check that `n_classes` is set correctly.
+
+    Introduced in https://github.com/[USER_REDACTED]n/scikit-learn/pull/33015"""
+
+    X, y = make_blobs(n_samples=150, centers=n_blobs, n_features=2, random_state=42)
+    clf = estimator.fit(X, y)
+    disp = DecisionBoundaryDisplay.from_estimator(clf, X, response_method="predict")
+    assert disp.n_classes == expected_n_classes
+
+    # Test that setting class_of_interest always converts to a binary problem.
+    disp_coi = DecisionBoundaryDisplay.from_estimator(
+        clf, X, class_of_interest=y[0], response_method="predict"
+    )
+    assert disp_coi.n_classes == 2
+

 def test_cmap_and_colors_logic(pyplot):
     """Check the handling logic for `cmap` and `colors`."""

I didn't check the details but from a cursory glance this looks great. Please feel free to ping me once CI is green.

I think it would be great to also find a way to include a non regression test for #32866, that is, that different regions are assigned distinct colors when `n_classes` is large.

EDIT: maybe inspecting the levels would be a way to achieve this, for instance by including/adapting the test of #32866.

Here is an early pass of feedback with a focus on the meaning of this display and the "n_classes" params when we use this to display the output of the `predict` method for a regressor.

LGTM besides the following minor details:

Sorry for all the comments.

Mostly nits. The only issues are issues that were probably introduced in #31553 or #29797 (by me, sorry!), which are quite specific so thought it was easier to detail them here. It would be worthwhile to fix these problems.
If you are not interested in fixing these issues in a subsequent PR, I can open a separate issue for them, but otherwise I will leave it in your hands!

Sorry another question and a nit.

I had another look at this PR and this LGTM as it is. It's already a big improvement. +1 for merge and tackling the remaining points discussed in linked issues in follow-up PRs.

Thanks for all your work on this, it was quite complicated in the end.

Some nits only and a question but LGTM and fine to merge.

Could we expand this test to check that `gist_rainbow` is used for larger number of classes?

I thought about that, too, but I think I'd rather do that in a follow-up PR because I also want to add another check (and test) for `multiclass_colors`: Currently we don't check if enough unique colors are available, so there will be duplicate colors for example when passing `"tab10"` with 11 classes.

I also noticed that there are some parameter checks for `multiclass_colors` in `from_estimator` and then some (again) in `plot`, where the colors are actually selected. Since `from_estimator` always calls `plot` (and `plot` could in principle also be called without `from_estimator`), I'd move all checks there, together with extending the tests.

In the end, I did decide to included it here, because more classes were also necessary for the non-regression test for https://github.com/[USER_REDACTED]n/scikit-learn/issues/32866.

This test will fail for ("tab10", 15), which I will address in a follow-up PR.

I had to combine colors and number of classes here because the list version has to match the exact number of classes.

This can be fixed by [NAME_REDACTED] in https://github.com/[USER_REDACTED]n/scikit-learn/pull/32867 once this is merged.

nit: we should stick to 2 space indentation in this bullet list.

```suggestion
        * None: 'tab10' colormap is used to sample colors if the number of
          classes is less than or equal to 10, otherwise 'gist_rainbow' colormap.
```

For which estimators is this needed? Isn't there a risk of unanticipated consequences of this generic `else` case?

Otherwise, I think we might want to raise a `ValueError` error or more specific exception with a message that is explicit about what we expected from the estimator.

In particular, if we pass a classifier without a `classes_` attribute, we might want to raise `NotFittedError` exception.

In particular, we should explicitly raise if the estimator is a regressor.

EDIT: actually, there is a test that checks that this display accepts regressors and I agree it can lead to a useful plot (in particular if we use `plot_method="pcolormesh"` for instance), but then the term "n_classes" is really weird and furthermore, I think it `n_classes` should not be set to the number of unique values but rather to None, and we should display the output of `predict` for a regressor as the output of `predict_proba` or `decision_function` for a binary classifier. Furthermore, we should use a non-rainbow colormap for this kind of outputs (e.g. Viridis or Blues).

BTW, is there an existing example somewhere that uses `DecisionBoundaryDisplay` on a regressor? If not, we might want to expand one.

Re: `NotFittedError`:
This is checked (and raised) in line 467. So to make the distinction more explicit, I'd change `hasattr(estimator, "classes_")` to `is_classifier(estimator)` here.

Re: `DecisionBoundaryDisplay` for regressor example:
I didn't find one, so I'll add one (in a follow-up PR).

Re: regressors:
The easiest adaption within the current code would be to set `n_classes` to 2 for regressors (and add this as a note in the docs), then the output is treated similar to `decision_function` for binary classification and the default color map is `viridis`.

I think we need to expand the tests for the regressor case (to make sure that it does not detect a "number of classes" equal to the number of data points).

Is there a reason to prefer this one over the other? I find the latter a bit easier to parse.
```suggestion
                            mask=(self.response != class_idx),
```

I prefer the suggested code (inner negation). It feels more direct to me and avoid one useless array allocation.

I would prefer to distinctly name and set the actual number of blobs in the data and the expected n_classes attribute value as distinct test parameters to better convey the expectations of this test.

```suggestion
[USER_REDACTED].mark.parametrize(
    "estimator, n_blobs, expected_n_classes",
    [
        (DecisionTreeClassifier(random_state=0), 7, 7),
        (DecisionTreeClassifier(random_state=0), 2, 2),
        (KMeans(n_clusters=7, random_state=0), 7, 7),
        (KMeans(n_clusters=2, random_state=0), 7, 2),
        (DecisionTreeRegressor(random_state=0), 7, 2),
        (IsolationForest(random_state=0), 7, 2),
    ],
)
def test_n_classes_attribute(pyplot, estimator, n_blobs, expected_n_classes):
    """Check that `n_classes` is set correctly.

    Introduced in https://github.com/[USER_REDACTED]n/scikit-learn/pull/33015"""

    X, y = make_blobs(n_samples=150, centers=n_blobs, n_features=2, random_state=42)
    clf = estimator.fit(X, y)

    disp = DecisionBoundaryDisplay.from_estimator(clf, X, response_method="predict")
    assert disp.n_classes == expected_n_classes

    # Test that setting class_of_interest always converts to a binary problem.
    disp_coi = DecisionBoundaryDisplay.from_estimator(
        clf, X, class_of_interest=y[0], response_method="predict"
    )
    assert disp_coi.n_classes == 2
```

Note: I have checked the code locally. It's possible I made a mistake while editing via the GitHub UI.

```suggestion
        Expected number of unique classes or labels if `response` was generated by a
        classifier or a clusterer.

        For (single output) regressors and outlier detectors, the continous response
        are displayed the same way as the continuous response of a binary
        classifier, hence, the `n_classes` attribute is always set to 2 by [NAME_REDACTED]
```

```suggestion
    # Part of a non-regression test for issue #32866 to make sure the colormap
    # used has enough distinct colors.
```

```suggestion
        # Part of a non-regression test for issue #32866 to make sure every class
        # has its own surface.
```

I don't think outlier detectors can have a continuous response, right? So maybe rather:
```suggestion
        Expected number of unique classes or labels if `response` was generated by a
        classifier or a clusterer.

        For (single output) regressors, the continous response is displayed the same way
        as the continuous response of a binary classifier, hence, the `n_classes`
        attribute is always set to 2 by [NAME_REDACTED]

        For outlier detectors, `n_classes` is set to 2 by definition (inlier or outlier).
```

Thanks! I added it manually and ran it again just to be sure.

nit pick, is it a class variable or an instance variable? Its value unique to each class instance right?

Not sure if we have to give this detail - especially as this only occurs if we use `from_estimator` and the estimator has `classes_` attribute.

We could just say it stores the number of classes/labels?

This was more a matplotlib bug that was propagated - and is it fixed for more recent versions of mpl? - if so maybe just mention fixed distinct colours bug for certain mpl versions?

```suggestion
  By :user:`[NAME_REDACTED] <AnneBeyer>`.
```
nit

I think we could just focus on the API fixes/changes, and not go into details about implementation, which the average user is probably not interested in and may be confusing.
i.e. we probably don't need to say that the classes are plotted one by [NAME_REDACTED] `multiclass_colors` is based on `n_classes` ?

Why 15? Is this to test for the https://github.com/[USER_REDACTED]n/scikit-learn/issues/32866 ? Though you only need >7 for this.

> the continuous response of a binary classifier

What about:

"the continuous unthresholded response of a binary classifier"
or just:
"the unthresholded response of a binary classifier"

Also lets add some links to the glossary (https://scikit-learn.org/dev/glossary.html -
you'll find which terms are included here), e.g. regressor, clusteror etc. Use following syntax, e.g.:
```
:term:`clusterer`
```

Should we add that for binary problems, specify color via kwargs?

Should we add a .. versionchanged:: here ...?

The comment isn't clear on why we are doing this, so I did some digging.

From https://github.com/[USER_REDACTED]n/scikit-learn/pull/31553#pullrequestreview-3005538868

> I just found that the tab10/tab20 special casing was removed, so I put it back because it gives better visualization (more distinguishable colors)

I think this will be the case for most [qualitative](https://matplotlib.org/stable/users/explain/colors/colormaps.html#qualitative) cmaps. i.e. for tab10:

`plt.get_cmap("tab10", 2)`, samples the colormap evenly, thus returns the first and last colors:

whereas the following gives the first 2 colors:

`plt.imshow([plt.get_cmap("tab10", 10).colors[:2]], aspect='auto')`

For a separate PR, maybe we could generalize this behaviour for all qualitative cmaps (these have 'colors' attr).

For colormaps with `colors` attr, `plt.get_cmap(cmap, self.n_classes)` will have a max number of colors.

e.g., `plt.get_cmap('tab10', 12)`

will only give 10 distinct colors - it will just start repeating colors.

In a separate PR, we should probably fix this and add a test.

```suggestion
                        # Get `LinearSegmentedColormap` for non-qualitative cmaps
```

unrelated but lets add for clarity

This is not fully accurate. Maybe:

"Plot only integer class values" or
"Plot only thresholded class values"

?

What does it look like if we plotted 'contourf' + predict like this? Does the automatic level determination by [NAME_REDACTED] off? Would specifying the levels to be integer values, 0-n_classes, help?

Then we could get rid of the condition at ~L300-305

Not sure if the bit about how regressors are handled is required.

Similar to above, we don't need to go into detail about implementation that may be confusing to users anyway.

```suggestion
            Specifies how to color each class when plotting :term:`multiclass` problems
```

I wonder if the bit about binary colors is better suited above in `multiclass_colors`?

same comment as below https://github.com/[USER_REDACTED]n/scikit-learn/pull/33015/changes#r2703251888

Do we support multioutput regressors? The "(single output)" raised this question for me.

What about:

For (single output) regressors `n_classes` should be set to 2 by convention (continuous response is displayed the same way as unthresholded binary responses).

If the user is instantiating their own class instance, they are setting this attr, and we don't infer anything. Thus we can only tell them what it should be set to

Maybe add this in the example here as well then?
https://github.com/[USER_REDACTED]n/scikit-learn/blob/main/doc/whats_new/upcoming_changes/README.md

I picked a random number >10 related to https://github.com/[USER_REDACTED]n/scikit-learn/pull/33015/changes#r2672075644 (and what you also observed below https://github.com/[USER_REDACTED]n/scikit-learn/pull/33015/changes#r2703156456), but we can change it to 11 to save some lines.

My initial motivation was to make the handling more similar for all plotting functions, but since `pcolormesh` also requires separate handling for `predict`, I agree that there is no reason to prefer this option over the one you suggest.

This will, however, require the specific level handling as proposed in #32867, but since this is also still required for `contour`, I will make the adjustments as you suggested, and then we can tackle the `level` problem in #32867.

Ah that is a good point. That is why I keep seeing changelogs without the full stop!

Ahh right. But if you give distinct colours (instead of e.g., 'tab10') does the problem: https://github.com/[USER_REDACTED]n/scikit-learn/pull/33015/changes#r2703156456 still occur?

Yeah I agree, it would be nice to simplify this code, because it is confusing but it seems different plots need to be handled differently. To summarise, atm all 'contour' is handled, then 'pcolormesh' predict, then in else we handle 'pcolormesh' continuous response and all 'contourf', which is a bit confusing.

Note my suggestion was mostly such that we only handle NON `predict` responses in the `else` condition.

Maybe we could add a short(ish) comment on why we need to handle pmesh with predict separately - just for easier reading later on?

The contour one is tricky to know what to do because problems noted in #33108 and https://github.com/[USER_REDACTED]n/scikit-learn/pull/32867#issuecomment-3723877765 so not sure what to suggest there.

> I will make the adjustments as you suggested

Will the plot still look okay with my suggestion?

Actually thinking about it more, what does 'pcolormesh' + predict look like if we handled it in the `else` condition, where we plot class by class? Even though we use a `LinearSegmentedColormap` which gives a gradient from white to the class colour, since the predict values are integer, the plot would be coloured with the class colour right?

It would be less efficient, but it would simplify code and maybe for displays, efficiency isn't as much of a concern?

Naive question, but why `<` ?

Does this occur if the user passes a list of colors, and we don't ensure it is the same length as `n_classes`?
But since we `for class_idx, cmap in enumerate(multiclass_cmaps):` later, wouldn't this be a problem?

Also looking more at #32866, it seems more of problem that the number levels is less than the number of classes.

Even if we passed the correct number of colors, because there are fewer levels, we would still not get the right number of colours plotted.

Sorry another nit - the colour handling has expanded from subsequent PRs and is now probably a bit more complex than it needs to be.

What about something simpler e.g.,

```python
if isinstance(self.multiclass_colors, list):
    colors = [mpl.colors.to_rgba(color) for color in self.multiclass_colors]
else:
    cmap_name = self.multiclass_colors
    if cmap_name is None:
        # use default if `multiclass_colors` None
        cmap_name = "tab10" if self.n_classes <= 10 else "gist_rainbow"
    elif not isinstance(cmap_name, str):
        raise ValueError("'multiclass_colors' must be a list or a str.")

    # Special case for the tab10 and tab20 colormaps that encode a
    # discrete set of colors that are easily distinguishable
    # contrary to other colormaps that are continuous.
    if cmap_name == "tab10" and self.n_classes <= 10:
        colors = plt.get_cmap("tab10", 10).colors[: self.n_classes]
    elif cmap_name == "tab20" and self.n_classes <= 20:
        colors = plt.get_cmap("tab20", 20).colors[: self.n_classes]
    else:
        cmap = plt.get_cmap(cmap_name, self.n_classes)
        if not hasattr(cmap, "colors"):
            # For LinearSegmentedColormap
            colors = cmap(np.linspace(0, 1, self.n_classes))
        else:
            colors = cmap.colors
```
Here we do the shorter condition (list) first, which reads easier than having the long condition first - so people don't forget whats going on.
It's similiar in length but less nesting.

Again, could be done in a separate PR.

How about this for now? (leaving non-plot `contour` separate for now, as this is being discussed separately here https://github.com/[USER_REDACTED]n/scikit-learn/issues/33108)

```suggestion
            if self.response.ndim == 2:  # predict
                # `pcolormesh` requires cmap
                cmap = mpl.colors.ListedColormap(colors)
                self.surface_ = plot_func(
                    self.xx0, self.xx1, self.response, cmap=cmap,**kwargs
                )

            # predict_proba or decision_function differ for plotting methods
            elif plot_method == "contour":
                # Plot only argmax
                self.surface_ = plot_func(
                    self.xx0, self.xx1, self.response.argmax(axis=2), colors=colors, **kwargs
                )
```

I didn't find a way to integrate it in the else condition, it kept giving an empty plot, but handling all `predict` versions first with a cmap, then non-predict `contour` and then the rest, would work, which I think I prefer. I changed it to that and also updated the description above with the new plot. This still requires the level fix from https://github.com/[USER_REDACTED]n/scikit-learn/pull/32867, but integrating this is planned anyways.

No, if you give it a list, it is checked to be the same length as the number of classes.

Oh, and I just remembered, I originally set it to >10 to test the correct application of the two default cases when `multiclass_colors` is `None` ("tab10" if `n_classes <= 10`, "gist_rainbow" otherwise). This then led to discovering the problem with explicitly passing "tab10" when `n_classes >10`

I'll change it to `==`. I think this was left over from before I noticed that lists have to match the exact number of classes.

This did help to discover the problem with "tab10" and `n_classes > 10` (which will be handled in a follow-up PR), but indeed it does not test the levels issue, which will be tackled separately.

I also stumbled across the "(single-output)".
Multi-output estimators are not supported. Should we make this explicit here as well (or rather in `response` below)? And maybe also in `from_estimator`?

This is the display class, which doesn't have `kwargs`. I will not tackle this here, but I was thinking: Since `multiclass_colors_` will be added to the instance once the colors are selected, do we need to store `multiclass_colors` here? I would rather pass it on as a parameter to `plot` (where the `kwargs` can then actually be set), but this will be part of #33115.

The current solution does not address this bug anymore, so I removed that part.

Noted in the new issue https://github.com/[USER_REDACTED]n/scikit-learn/issues/33115

Would that be version 1.9?
And it should simply say this, right?
`multiclass_colors` is now also used when `response_method="predict"`

Agreed, but related to https://github.com/[USER_REDACTED]n/scikit-learn/pull/33015#discussion_r2712516746 above, should we add a note that multi-output estimators are not supported?

See https://github.com/[USER_REDACTED]n/scikit-learn/pull/33015#discussion_r2712546036

Noted as part of #33115

I added this suggestion and updated the resulting plots in the PR description above.

+1 for making it explicit that multi-output regressors are not supported by raising a `ValueError` if the `predict` method of a regressors output more than one values per input data points (i.e. `responses.ndim == 2 and responses.shape[1] > 1`).

Feel free to rephrase the doc accordingly, but in my opinion the error message should be enough to make that explicit.

Yes.

A `ValueError` is already raised for multi-output classifiers in line 49 and for  regressors in line 575 (which could be refactored at some point to happen in the same place).

I will leave the docstrings as they are, then.

I added it to be explicit for now, keeping in mind that making binary/multiclass use the same parameter is on the agenda as well.

We decided above that the error message is enough for now.

Sorry, I don't quite follow here, why 11? If we are testing just a list of colours (and not using default 'tab10'), why does the list of colours need to be >10?

Sorry I don't quite follow why 11 - I asked here: https://github.com/[USER_REDACTED]n/scikit-learn/pull/33015#discussion_r2748506453

Nit/question, what do you think about having these 2 items as bullet points? (I'm okay either way)

Unrelated, I stumbled on this when I re-read it, "Single color colormaps" is not so intuitive - I think we create a white to single colour gradient colour map here, but not sure if this is reflected in "Single color colormaps". What do you think about:

"Single color (fading to white) colormaps"  or
"Single color, decreasing in saturation to white, colormaps" ?

Ah, now I get it, you're right, the 11 test case is only necessary for the None condition (and will be for "tab10", but that will only be added later). I removed the redundant tests.

I'd leave it like it is, because these two are equal options to the line above (that describes classifiers and clusterers).

I find it quite self-explanatory, but I have looked at the output a lot now, so I don't mind adding it for clarity.

Yeah, I guess it depends on how familiar you are with mpl's colormaps...


FIX Resolve precompute in enet_path when check_input is False #32989

<!--
🙌 Thanks for contributing a pull request!

👀 Please ensure you have taken a look at the contribution guidelines:
https://github.com/[USER_REDACTED]n/scikit-learn/blob/main/CONTRIBUTING.md

✅ In particular following the pull request checklist will increase the likelihood
of having maintainers review your PR:
https://scikit-learn.org/dev/developers/contributing.html#pull-request-checklist

📋 If your PR is likely to affect users, you will need to add a changelog entry
describing your PR changes, see:
https://github.com/[USER_REDACTED]n/scikit-learn/blob/main/doc/whats_new/upcoming_changes/README.md
-->

#### [NAME_REDACTED]/PRs
<!--
Example: Fixes #1234. See also #3456.
Please use keywords (e.g., Fixes) to create link to the issues or pull requests
you resolved, so that they will automatically be closed when your pull request
is merged. See https://github.com/blog/1506-closing-issues-via-pull-requests
-->
Fixes #32989

#### What does this implement/fix? Explain your changes.
This PR fixes a `ValueError` in `enet_path` when `check_input=False`.

Because `_pre_fit` is skipped when `check_input=False`, the default value of the precompute parameter remained as the string 'auto', causing a crash in the solver dispatch logic. I have added a resolution block to handle 'auto', True, and sparse matrix constraints manually, ensuring the correct Gram matrix and Xy are available for the solver without duplicating the full validation suite.

A non-regression test has been added to `test_coordinate_descent.py`.

#### AI usage disclosure
<!--
If AI tools were involved in creating this PR, please check all boxes that apply
below and make sure that you adhere to our [NAME_REDACTED]:
https://scikit-learn.org/dev/developers/contributing.html#automated-contributions-policy
-->
I used AI assistance for:
- [ ] Code generation (e.g., when writing an implementation or fixing a bug)
- [ ] Test/benchmark generation
- [ ] Documentation (including examples)
- [x] Research and understanding

#### Any other comments?
First-time contributor here! I've followed the internal logic of `_pre_fit` as closely as possible while maintaining performance and memory efficiency.

<!--
Thank you for your patience. Changes to scikit-learn require careful
attention, but with limited maintainer time, not every contribution can be reviewed
quickly.
For more information and tips on improving your pull request, see:
https://scikit-learn.org/dev/faq.html#why-is-my-pull-request-not-getting-any-attention.

Thanks for contributing!
-->

@@ -0,0 +1,6 @@
+- :func:`linear_model.enet_path` now correctly handles the ``precompute``
+  parameter when ``check_input=False``. Previously, the value of
+  ``precompute`` was not properly treated which could lead to a ValueError.
+  This also affects :class:`linear_model.ElasticNetCV`, :class:`linear_model.LassoCV`,
+  :class:`linear_model.MultiTaskElasticNetCV` and :class:`linear_model.MultiTaskLassoCV`.
+  By :user:`[NAME_REDACTED] <adc-trust-ai>`

@@ -633,15 +633,15 @@ def enet_path(

     # X should have been passed through _pre_fit already if function is called
     # from ElasticNet.fit
-    if check_input:
+    if check_input or precompute is not False:
         X, y, _, _, _, precompute, Xy = _pre_fit(
             X,
             y,
             Xy,
             precompute,
             fit_intercept=False,
             copy=False,
-            check_gram=True,
+            check_gram=check_input,
         )
     if alphas is None:
         # fit_intercept and sample_weight have already been dealt with in calling

@@ -1875,3 +1875,11 @@ def test_linear_model_cv_alphas(Estimator):
     else:
         clf.fit(X, y[:, 0])
     assert len(clf.alphas_) == 100
+
+
+[USER_REDACTED].mark.parametrize("precompute", ["auto", True, False])
+def test_enet_path_check_input_false(precompute):
+    """Test enet_path works with check_input=False and various precompute settings."""
+    X, y = make_regression(n_samples=100, n_features=5, n_informative=2, random_state=0)
+    X = np.asfortranarray(X)
+    alphas, _, _ = enet_path(X, y, n_alphas=3, check_input=False, precompute=precompute)

Hi [USER_REDACTED] thank you for this PR, to get the ball rolling I added one nitpick for now, LGTM but will let [USER_REDACTED] also take a look to check the logic when check_input=False

Thank you for your work, [USER_REDACTED].
I only have some minor comments on the test structure and the multi_output test case doesn't need to be added at all.

I agree we should mention all the affected functions and methods in the changelog entry.

Otherwise looks fine. Approving.

Thanks for your work, [USER_REDACTED]!

I suppose since auto is the default we can remove it here and recomment this into ```Case 1: Testing default="auto"```

```suggestion
    alphas, _, _ = enet_path(X, y, n_alphas=3, check_input=False)
```

What was wrong, what does it  fix?

```suggestion
    """Test that ...."""
```

Please put imports at the top of the file. As both are already imported, simply delete it here.

```suggestion
```

Can't we call _pre_fit or some other function here instead of replicating its logic?

Great point. I've made it more descriptive.

Done. Good catch.

Good suggestion, it's done now.

Definitely. Originally, I didn't call _pre_fit to avoid unnecessary operations and overhead, but I believe essentially the same effect can be achieved by [NAME_REDACTED] `fit_intercept=False` and `copy=False`. By calling _pre_fit, the code is easier to maintain, so it was a great suggestion. Thanks!

This can be removed here. A few lines further down, _pre_fit is called. Just the if-condition has to be extended.

Done. I've updated the PR to remove the if block you mentioned and instead extend the existing if condition: `if check_input or precompute is not False`.
This ensures _pre_fit is called whenever setup is required (resolving 'auto', True, or arrays) while maintaining the optimization to skip the call entirely when `check_input=False` and `precompute=False`. Verified across all parameter combinations.

Isn‘t it also `lasso_path`?

„Previously, the value of `precompute` was not properly treated which could lead to a ValueError.“
Just a suggestion.

```suggestion
```

This is uncommon. But instead you can add "Regression test for #32989" in the test docstring.

```suggestion
```

We also don't need these comments. Instead you could parametrise the precompute param with `[USER_REDACTED].mark.parametrize` which also makes the different test cases more visible and debuggable in case of future regressions.

Why do we need to assert `len(alphas) == 3` here? `enet_path` raised during execution before, so testing if it can be called without errors should be enough?

I would think we don't need this test case, since in the "multi_output" case, `precompute` is not used at all.

This is fine. When `precompute` is a matrix, it gets passed through `_pre_fit`.

I am not the biggest fan of issue numbers in test descriptions. I prefer a docstring or code comment that explains what is tested (with or without issue number): "Test that ..." (The unittest package for R is named [testthat](https://testthat.r-lib.org/))

I agree, it can be safely removed. I added it just out of habit (I tend to add a simple check in my own code debugging to verify a certain critical line ran as intended).

I added it to reach the codecov threshold, as otherwise it would just fall short by a couple percentage points. I will try deleting it and checking codecov again.

Excellent, thanks.

I wrote enet_path because that's where I came across the bug. But I think you're right, as any code that relies on _coordinate_descent.py should benefit from this fix. I can also add a reference to `lasso_path`if you feel it will be helpful.

Sounds good, thanks!

Great suggestion. It's done now.

That's why I had suggested to _add_ it, [USER_REDACTED].
It is also fine as it is.

I see. Now the replicated code is gone, the replicated test is also not needed. Happy that codecov is happy. :woman_dancing:

>  I added it just out of habit

Sure, I understand. We need to be a bit nit picky in general, since scikit-learn hopefully is in need to stay maintainable for another 19 years and more.

And as a heavy user of scikit-learn I am glad you are "nit picky"!

```suggestion
    if check_input or precompute:
```
Not 100% sure. Do we want to enter the if-condition if precompute is an ndarray?

I see your logic: if `check_input=False` then, to respect the current documentation for `enet_path`, the Gram should not be checked. To align with the documentation while ensuring the solver remains robust, I propose keeping the `precompute is not False` check (to avoid the NumPy truth-value crash that would happen with just `precompute`) but update the call to `_pre_fit`: by setting `check_gram=check_input`, we ensure that if a user specifies `check_input=False`, the Gram matrix validation is indeed skipped as documented, while still allowing `_pre_fit` to handle the essential memory alignment and correct Xy initialization required by [NAME_REDACTED]

Then the following reads better:
```suggestion
    if check_input or precompute is True:  # numpy arrays as precompute must not enter
```

I'm concerned that `if precompute is True` would reintroduce the very bug we are trying to fix: when `precompute="auto"` (the default), the block would be skipped and the solver would receive a string instead of the required data.

Yes, if a matrix input should be excluded, we still need to run `_pre_fit` for `"auto"` and `True` inputs.

Agreed. To be clear, `_pre_fit` is explicitly designed to handle ndarray inputs (for memory alignment and Xy initialization). There's no fundamental reason to exclude them from the block aside from the validation overhead, which I proposed to address by passing `check_gram=check_input`. This approach aligns with the `enet_path` documentation while remaining efficient, robust, and future-proof.

I don’t understand. `x is True` and `x is not False` evaluate to the same result - or I don‘t understand Python!

In Python, `x is not False` is not equivalent to `x is True` because the former checks if `x` is something other than the singleton `False` object. In this module, `precompute` can be a boolean but also the string `"auto"` (the default) or an `ndarray`. For both strings and arrays, `is True` would evaluate to `False`, skipping the required initialization, whereas `is not False` correctly evaluates to `True`.

That's right.


FIX remove the special check in `test_enet_ols_consistency`

<!--
🙌 Thanks for contributing a pull request!

👀 Please ensure you have taken a look at the contribution guidelines:
https://github.com/[USER_REDACTED]n/scikit-learn/blob/main/CONTRIBUTING.md

✅ In particular following the pull request checklist will increase the likelihood
of having maintainers review your PR:
https://scikit-learn.org/dev/developers/contributing.html#pull-request-checklist

📋 If your PR is likely to affect users, you will need to add a changelog entry
describing your PR changes, see:
https://github.com/[USER_REDACTED]n/scikit-learn/blob/main/doc/whats_new/upcoming_changes/README.md
-->

#### [NAME_REDACTED]/PRs
<!--
Example: Fixes #1234. See also #3456.
Please use keywords (e.g., Fixes) to create link to the issues or pull requests
you resolved, so that they will automatically be closed when your pull request
is merged. See https://github.com/blog/1506-closing-issues-via-pull-requests
-->
Fixes #32987

#### What does this implement/fix? Explain your changes.
- This removes the special condition that checks `se_enet <= se_ols`

#### AI usage disclosure
<!--
If AI tools were involved in creating this PR, please check all boxes that apply
below and make sure that you adhere to our [NAME_REDACTED]:
https://scikit-learn.org/dev/developers/contributing.html#automated-contributions-policy
-->
I used AI assistance for:
- [ ] Code generation (e.g., when writing an implementation or fixing a bug)
- [ ] Test/benchmark generation
- [ ] Documentation (including examples)
- [x] Research and understanding

#### Any other comments?
CC: [USER_REDACTED]

<!--
Thank you for your patience. Changes to scikit-learn require careful
attention, but with limited maintainer time, not every contribution can be reviewed
quickly.
For more information and tips on improving your pull request, see:
https://scikit-learn.org/dev/faq.html#why-is-my-pull-request-not-getting-any-attention.

Thanks for contributing!
-->

@@ -1607,9 +1607,9 @@ def test_enet_ridge_consistency(ridge_alpha, precompute, n_targets):
 [USER_REDACTED].mark.filterwarnings("ignore:With alpha=0, this algorithm:UserWarning")
 [USER_REDACTED].mark.parametrize("precompute", [False, True])
 [USER_REDACTED].mark.parametrize("effective_rank", [None, 10])
-def test_enet_ols_consistency(precompute, effective_rank):
+def test_enet_ols_consistency(precompute, effective_rank, global_random_seed):
     """Test that ElasticNet(alpha=0) converges to the same solution as OLS."""
-    rng = np.random.RandomState(42)
+    rng = np.random.RandomState(global_random_seed)
     n_samples = 300
     X, y = make_regression(
         n_samples=n_samples,
@@ -1628,16 +1628,13 @@ def test_enet_ols_consistency(precompute, effective_rank):
     # Might be a singular problem, so check for same predictions
     assert_allclose(enet.predict(X), ols.predict(X))
     # and for similar objective function (squared error)
-    se_ols = np.sum((y - ols.predict(X)) ** 2)
-    se_enet = np.sum((y - enet.predict(X)) ** 2)
-    if precompute:
-        assert se_ols <= 1e-20
-        assert se_enet <= 1e-20
-    else:
-        assert se_enet <= se_ols <= 1e-20  # Who would have thought that?
+    se_ols = np.sum(sw * (y - ols.predict(X)) ** 2)
+    se_enet = np.sum(sw * (y - enet.predict(X)) ** 2)
+    assert se_ols <= 1e-19
+    assert se_enet <= 1e-19
     # We check equal coefficients, but "only" with absolute tolerance.
     assert_allclose(enet.coef_, ols.coef_, atol=1e-11)
-    assert_allclose(enet.intercept_, ols.intercept_, atol=1e-12)
+    assert_allclose(enet.intercept_, ols.intercept_, atol=1e-11)

 [USER_REDACTED].mark.parametrize(

[USER_REDACTED] Thanks for the fix.
My suggestions pass locally for all random seeds (`SKLEARN_TESTS_GLOBAL_RANDOM_SEED="all" pytest -x sklearn/linear_model/tests/test_coordinate_descent.py::test_enet_ols_consistency`).

```suggestion
se_ols = np.sum(sw * (y - ols.predict(X)) ** 2)
se_enet = np.sum(sw * (y - enet.predict(X)) ** 2)
```

```suggestion
    assert se_ols <= 1e-19
    assert se_enet <= 1e-19
```

```suggestion
    assert_allclose(enet.intercept_, ols.intercept_, atol=1e-11)
```


FEA: Add Array API support to `pairwise_distances_argmin`

<!--
🙌 Thanks for contributing a pull request!

👀 Please ensure you have taken a look at the contribution guidelines:
https://github.com/[USER_REDACTED]n/scikit-learn/blob/main/CONTRIBUTING.md

✅ In particular following the pull request checklist will increase the likelihood
of having maintainers review your PR:
https://scikit-learn.org/dev/developers/contributing.html#pull-request-checklist

📋 If your PR is likely to affect users, you will need to add a changelog entry
describing your PR changes, see:
https://github.com/[USER_REDACTED]n/scikit-learn/blob/main/doc/whats_new/upcoming_changes/README.md
-->

#### [NAME_REDACTED]/PRs
<!--
Example: Fixes #1234. See also #3456.
Please use keywords (e.g., Fixes) to create link to the issues or pull requests
you resolved, so that they will automatically be closed when your pull request
is merged. See https://github.com/blog/1506-closing-issues-via-pull-requests
-->
Towards #26024

#### What does this implement/fix? Explain your changes.
Adds array API support for `pairwise_distances_argmin`

#### AI usage disclosure
<!--
If AI tools were involved in creating this PR, please check all boxes that apply
below and make sure that you adhere to our [NAME_REDACTED]:
https://scikit-learn.org/dev/developers/contributing.html#automated-contributions-policy
-->
I used AI assistance for:
- [x] Code generation (e.g., when writing an implementation or fixing a bug)
- [ ] Test/benchmark generation
- [ ] Documentation (including examples)
- [x] Research and understanding

#### Any other comments?

<!--
Thank you for your patience. Changes to scikit-learn require careful
attention, but with limited maintainer time, not every contribution can be reviewed
quickly.
For more information and tips on improving your pull request, see:
https://scikit-learn.org/dev/faq.html#why-is-my-pull-request-not-getting-any-attention.

Thanks for contributing!
-->

@@ -200,6 +200,7 @@ Metrics
 - :func:`sklearn.metrics.pairwise.cosine_similarity`
 - :func:`sklearn.metrics.pairwise.cosine_distances`
 - :func:`sklearn.metrics.pairwise.pairwise_distances` (only supports "cosine", "euclidean", "manhattan" and "l2" metrics)
+- :func:`sklearn.metrics.pairwise.pairwise_distances_argmin`
 - :func:`sklearn.metrics.pairwise.euclidean_distances` (see :ref:`device_support_for_float64`)
 - :func:`sklearn.metrics.pairwise.laplacian_kernel`
 - :func:`sklearn.metrics.pairwise.linear_kernel`

@@ -0,0 +1,2 @@
+- :func:`sklearn.metrics.pairwise.pairwise_distances_argmin` now supports array API
+  compatible inputs. By :user:`[NAME_REDACTED] <bharatr21>`.

@@ -650,7 +650,8 @@ def _argmin_reduce(dist, start):
     # `start` is specified in the signature but not used. This is because the higher
     # order `pairwise_distances_chunked` function needs reduction functions that are
     # passed as argument to have a two arguments signature.
-    return dist.argmin(axis=1)
+    xp, _ = get_namespace(dist)
+    return xp.argmin(dist, axis=1)

 _VALID_METRICS = [
@@ -937,14 +938,15 @@ def pairwise_distances_argmin(X, Y, *, axis=1, metric="euclidean", metric_kwargs
     """
     ensure_all_finite = "allow-nan" if metric == "nan_euclidean" else True
     X, Y = check_pairwise_arrays(X, Y, ensure_all_finite=ensure_all_finite)
+    xp, _ = get_namespace(X, Y)

     if axis == 0:
         X, Y = Y, X

     if metric_kwargs is None:
         metric_kwargs = {}

-    if ArgKmin.is_usable_for(X, Y, metric):
+    if ArgKmin.is_usable_for(X, Y, metric) and _is_numpy_namespace(xp):
         # This is an adaptor for one "sqeuclidean" specification.
         # For this backend, we can directly use "sqeuclidean".
         if metric_kwargs.get("squared", False) and metric == "euclidean":
@@ -972,14 +974,13 @@ def pairwise_distances_argmin(X, Y, *, axis=1, metric="euclidean", metric_kwargs
         # Turn off check for finiteness because this is costly and because arrays
         # have already been validated.
         with config_context(assume_finite=True):
-            indices = np.concatenate(
+            indices = xp.concat(
                 list(
-                    # This returns an np.ndarray generator whose arrays we need
-                    # to flatten into one.
                     pairwise_distances_chunked(
                         X, Y, reduce_func=_argmin_reduce, metric=metric, **metric_kwargs
                     )
-                )
+                ),
+                axis=0,
             )

     return indices

@@ -74,6 +74,7 @@
     paired_euclidean_distances,
     paired_manhattan_distances,
     pairwise_distances,
+    pairwise_distances_argmin,
     pairwise_kernels,
     polynomial_kernel,
     rbf_kernel,
@@ -2454,6 +2455,7 @@ def check_array_api_metric_pairwise(metric, array_namespace, device, dtype_name)
         check_array_api_binary_classification_metric,
     ],
     pairwise_distances: [check_array_api_metric_pairwise],
+    pairwise_distances_argmin: [check_array_api_metric_pairwise],
 }

LGTM. Thanks [USER_REDACTED]

LGTM. Thank you [USER_REDACTED]

Is it safe to assume that any array namespace will have the `concat` method? If so, it makes the fallback unnecessary and I'll remove it. Requesting confirmation from a reviewer

This line seems to be impacting code coverage too

https://data-apis.org/array-api/latest/API_specification/generated/array_api.concat.html#concat is the specification in the array API for `concat`.

This means it should always be available and we don't need this `if` statement and the comment

Instead of the diff (adding a new variable `indices_list`), you could just change `np.concatenate` to `xp.concat`. I think.

Updated


DOC Switch insecure http links to https

I updated several insecure `http` links to `https` in the documentation.

Files updated:
* `doc/about.rst`
* `doc/developers/contributing.rst`
* `doc/developers/performance.rst`

@@ -385,13 +385,13 @@ program.
 - 2013 - [NAME_REDACTED], Nicolas Trésegnie
 - 2014 - [NAME_REDACTED], [NAME_REDACTED], [NAME_REDACTED], [NAME_REDACTED]
 - 2015 - `Raghav RV <https://github.com/[USER_REDACTED]>`_, [NAME_REDACTED]
-- 2016 - `[NAME_REDACTED] <http://nelsonliu.me>`_, `[NAME_REDACTED] <https://yenchenlin.me/>`_
+- 2016 - `[NAME_REDACTED] <https://nelsonliu.me>`_, `[NAME_REDACTED] <https://yenchenlin.me/>`_

 .. _Vlad Niculae: https://vene.ro/

 ...................

-The `NeuroDebian <http://neuro.debian.net>`_ project providing `Debian
+The `NeuroDebian <https://neuro.debian.net>`_ project providing `Debian
 <https://www.debian.org/>`_ packaging and contributions is supported by
 `Dr. James V. Haxby <http://haxbylab.dartmouth.edu/>`_ (`[NAME_REDACTED] <https://pbs.dartmouth.edu/>`_).

@@ -910,7 +910,7 @@ def setup(app):
     r"^..?/",
     # ignore links to specific pdf pages because linkcheck does not handle them
     # ('utf-8' codec can't decode byte error)
-    r"http://www.utstat.toronto.edu/~rsalakhu/sta4273/notes/Lecture2.pdf#page=.*",
+    r"https://www.utstat.toronto.edu/~rsalakhu/sta4273/notes/Lecture2.pdf#page=.*",
     (
         "https://www.fordfoundation.org/media/2976/roads-and-bridges"
         "-the-unseen-labor-behind-our-digital-infrastructure.pdf#page=.*"

@@ -358,7 +358,7 @@ line
 .. topic:: [NAME_REDACTED] `Git documentation <https://git-scm.com/doc>`_ and
-    http://try.github.io are excellent resources to get started with git,
+    https://try.github.io are excellent resources to get started with git,
     and understanding all of the commands shown here.

 .. _pr_checklist:
@@ -508,7 +508,7 @@ profiling and Cython optimizations.

    For two very well documented and more detailed guides on development
    workflow, please pay a visit to the `[NAME_REDACTED]
-   <http://scipy.github.io/devdocs/dev/dev_quickstart.html>`_ -
+   <https://scipy.github.io/devdocs/dev/dev_quickstart.html>`_ -
    and the `[NAME_REDACTED] for Developers
    <https://astropy.readthedocs.io/en/latest/development/workflow/development_workflow.html>`_
    sections.

@@ -311,7 +311,7 @@ standalone function in a ``.pyx`` file, add static type declarations and
 then use Cython to generate a C program suitable to be compiled as a
 Python extension module.

-The `Cython's documentation <http://docs.cython.org/>`_ contains a tutorial and
+The `Cython's documentation <https://docs.cython.org/>`_ contains a tutorial and
 reference guide for developing such a module.
 For more information about developing in Cython for scikit-learn, see :ref:`cython`.

@@ -295,7 +295,7 @@ It can be installed using ``dnf``:
 NetBSD
 ------

-scikit-learn is available via `pkgsrc-wip <http://pkgsrc-wip.sourceforge.net/>`_:
+scikit-learn is available via `pkgsrc-wip <https://pkgsrc-wip.sourceforge.net/>`_:
 https://pkgsrc.se/math/py-scikit-learn

@@ -149,7 +149,7 @@ facilitate the conversion of the data models between different machine learning
 frameworks, and to improve their portability on different computing
 architectures. More details are available from the `ONNX tutorial
 <https://onnx.ai/get-started.html>`__. To convert scikit-learn model to `ONNX`
-`sklearn-onnx <http://onnx.ai/sklearn-onnx/>`__ has been developed. However,
+`sklearn-onnx <https://onnx.ai/sklearn-onnx/>`__ has been developed. However,
 not all scikit-learn models are supported, and it is limited to the core
 scikit-learn and does not support most third party estimators. One can write a
 custom converter for third party or custom estimators, but the documentation to
@@ -159,7 +159,7 @@ do that is sparse and it might be challenging to do so.

   To convert the model to `ONNX` format, you need to give the converter some
   information about the input as well, about which you can read more `here
-  <http://onnx.ai/sklearn-onnx/index.html>`__::
+  <https://onnx.ai/sklearn-onnx/index.html>`__::

       from skl2onnx import to_onnx
       onx = to_onnx(clf, X[:1].astype(numpy.float32), target_opset=12)

@@ -847,7 +847,7 @@ clusters from Bisecting K-Means are well ordered and create quite a visible hier
 .. dropdown:: References

   * `"A Comparison of [NAME_REDACTED]"
-    <http://www.philippe-fournier-viger.com/spmf/bisectingkmeans.pdf>`_ Michael
+    <https://www.philippe-fournier-viger.com/spmf/bisectingkmeans.pdf>`_ [NAME_REDACTED], [NAME_REDACTED] and [NAME_REDACTED], Department of [NAME_REDACTED] and
     Egineering, University of Minnesota (June 2000)
   * `"[NAME_REDACTED] of K-Means and Bisecting K-[NAME_REDACTED] in Weblog
@@ -1584,7 +1584,7 @@ Bad (e.g. independent labelings) have non-positive scores::
   * Strehl, Alexander, and [NAME_REDACTED] (2002). "Cluster ensembles - a
     knowledge reuse framework for combining multiple partitions". Journal of
     [NAME_REDACTED] 3: 583-617. `doi:10.1162/153244303321897735
-    <http://strehl.com/download/strehl-jmlr02.pdf>`_.
+    <https://strehl.com/download/strehl-jmlr02.pdf>`_.

   * `Wikipedia entry for the (normalized) [NAME_REDACTED]
     <https://en.wikipedia.org/wiki/Mutual_Information>`_
@@ -1769,7 +1769,7 @@ homogeneous but not complete::
   Hirschberg, 2007

 .. [B2011] `Identification and Characterization of Events in [NAME_REDACTED]
-  <http://www.cs.columbia.edu/~hila/hila-thesis-distributed.pdf>`_, Hila
+  <https://www.cs.columbia.edu/~hila/hila-thesis-distributed.pdf>`_, [NAME_REDACTED], [NAME_REDACTED].

@@ -1022,5 +1022,5 @@ computation and thus speeds it up.
 .. dropdown:: References

   * Ojala and Garriga. `[NAME_REDACTED] for [NAME_REDACTED]
-    <http://www.jmlr.org/papers/volume11/ojala10a/ojala10a.pdf>`_.
+    <https://www.jmlr.org/papers/volume11/ojala10a/ojala10a.pdf>`_.
     J. Mach. Learn. Res. 2010.

@@ -950,7 +950,7 @@ is not readily available from the start, or when the data does not fit into memo
 .. rubric:: References

 .. [1] `"Learning the parts of objects by [NAME_REDACTED]"
-  <http://www.cs.columbia.edu/~blei/fogm/2020F/readings/LeeSeung1999.pdf>`_
+  <https://www.cs.columbia.edu/~blei/fogm/2020F/readings/LeeSeung1999.pdf>`_
   D. Lee, S. Seung, 1999

 .. [2] `"[NAME_REDACTED] with [NAME_REDACTED]"

@@ -536,7 +536,7 @@ additional information related to the successive halving process.

   .. [1] K. Jamieson, A. Talwalkar,
      `[NAME_REDACTED] Identification and Hyperparameter
-     Optimization <http://proceedings.mlr.press/v51/jamieson16.html>`_, in
+     Optimization <https://proceedings.mlr.press/v51/jamieson16.html>`_, in
      proc. of [NAME_REDACTED], 2016.

   .. [2] L. Li, K. Jamieson, G. DeSalvo, A. Rostamizadeh, A. Talwalkar,

@@ -1448,7 +1448,7 @@ eta0=1.0)` can be used for PA-I or with ``learning_rate="pa2"`` for PA-II.
 .. dropdown:: References

   * `"[NAME_REDACTED]"
-    <http://jmlr.csail.mit.edu/papers/volume7/crammer06a/crammer06a.pdf>`_
+    <https://jmlr.csail.mit.edu/papers/volume7/crammer06a/crammer06a.pdf>`_
     K. Crammer, O. Dekel, J. Keshat, S. Shalev-Shwartz, Y. Singer - JMLR 7 (2006)

 Robustness regression: outliers and modeling errors
@@ -1655,7 +1655,7 @@ better than an ordinary least squares in high dimension.

   .. [#f1] [NAME_REDACTED], [NAME_REDACTED], [NAME_REDACTED] and [NAME_REDACTED]: `[NAME_REDACTED] in a [NAME_REDACTED] Model. <http://home.olemiss.edu/~xdang/papers/MTSE.pdf>`_

-  .. [#f2] T. Kärkkäinen and S. Äyrämö: `On Computation of [NAME_REDACTED] for [NAME_REDACTED]. <http://users.jyu.fi/~samiayr/pdf/ayramo_eurogen05.pdf>`_
+  .. [#f2] T. Kärkkäinen and S. Äyrämö: `On Computation of [NAME_REDACTED] for [NAME_REDACTED]. <https://users.jyu.fi/~samiayr/pdf/ayramo_eurogen05.pdf>`_

   Also see the `Wikipedia page <https://en.wikipedia.org/wiki/Theil%E2%80%93Sen_estimator>`_

@@ -2009,7 +2009,7 @@ the same does a lower Brier score loss always mean better calibration"

 .. [Bella2012] Bella, Ferri, Hernández-Orallo, and Ramírez-Quintana
   `"Calibration of [NAME_REDACTED]"
-  <http://dmip.webs.upv.es/papers/BFHRHandbook2010.pdf>`_
+  <https://dmip.webs.upv.es/papers/BFHRHandbook2010.pdf>`_
   in Khosrow-Pour, M. "Machine learning: concepts, methodologies, tools
   and applications." Hershey, PA: [NAME_REDACTED] (2012).

@@ -813,4 +813,4 @@ used, please refer to their respective papers.

 .. [#8] Crammer and Singer `On the [NAME_REDACTED] of [NAME_REDACTED] Machines
-  <http://jmlr.csail.mit.edu/papers/volume2/crammer01a/crammer01a.pdf>`_, JMLR 2001.
+  <https://jmlr.csail.mit.edu/papers/volume2/crammer01a/crammer01a.pdf>`_, JMLR 2001.

@@ -310,7 +310,7 @@ the lower half of those faces.

 * M. Dumont et al,  `Fast multi-class image annotation with random subwindows
   and multiple output randomized trees
-  <http://www.montefiore.ulg.ac.be/services/stochastic/pubs/2009/DMWG09/dumont-visapp09-shortpaper.pdf>`_,
+  <https://www.montefiore.ulg.ac.be/services/stochastic/pubs/2009/DMWG09/dumont-visapp09-shortpaper.pdf>`_,
   [NAME_REDACTED] on [NAME_REDACTED] and Applications 2009

 .. _tree_complexity:

@@ -390,8 +390,8 @@ Who is using scikit-learn?
       :target: https://www.phimeca.com/?lang=en

-`HowAboutWe <http://www.howaboutwe.com/>`_
-------------------------------------------
+`HowAboutWe <https://www.howaboutwe.com/>`_
+-------------------------------------------

 .. div:: sk-[REDACTED]

@@ -413,7 +413,7 @@ Who is using scikit-learn?
   .. div:: image-box

     .. image:: images/howaboutwe.png
-      :target: http://www.howaboutwe.com/
+      :target: https://www.howaboutwe.com/

 `PeerIndex <https://www.brandwatch.com/peerindex-and-brandwatch>`_
@@ -598,8 +598,8 @@ Who is using scikit-learn?
       :target: https://www.solidodesign.com/

-`INFONEA <http://www.infonea.com/en/>`_
----------------------------------------
+`INFONEA <https://www.infonea.com/en/>`_
+----------------------------------------

 .. div:: sk-[REDACTED]

@@ -620,7 +620,7 @@ Who is using scikit-learn?
   .. div:: image-box

     .. image:: images/infonea.jpg
-      :target: http://www.infonea.com/en/
+      :target: https://www.infonea.com/en/

 `Dataiku <https://www.dataiku.com/>`_

@@ -22,11 +22,11 @@

 .. _Olivier Grisel: https://bsky.app/profile/ogrisel.bsky.social

-.. _Gael Varoquaux: http://gael-varoquaux.info
+.. _Gael Varoquaux: https://gael-varoquaux.info

-.. _Alexandre Gramfort: http://alexandre.gramfort.net
+.. _Alexandre Gramfort: https://alexandre.gramfort.net

-.. _Fabian Pedregosa: http://fa.bianp.net
+.. _Fabian Pedregosa: https://fa.bianp.net

 .. _Mathieu Blondel: http://www.mblondel.org

@@ -42,7 +42,7 @@

 .. _Peter Prettenhofer: https://sites.google.com/site/peterprettenhofer/

-.. _Alexandre Passos: http://atpassos.me
+.. _Alexandre Passos: https://atpassos.me

 .. _Nicolas Pinto: https://twitter.com/npinto

@@ -54,7 +54,7 @@

 .. _Jake Vanderplas: https://staff.washington.edu/jakevdp/

-.. _Gilles Louppe: http://www.montefiore.ulg.ac.be/~glouppe/
+.. _Gilles Louppe: https://www.montefiore.ulg.ac.be/~glouppe/

 .. _INRIA: https://www.inria.fr/

@@ -90,21 +90,21 @@

 .. _Kyle Kastner: https://kastnerkyle.github.io/

-.. _Daniel Nouri: http://danielnouri.org
+.. _Daniel Nouri: https://danielnouri.org

 .. _Manoj Kumar: https://manojbits.wordpress.com

-.. _Luis [NAME_REDACTED]: http://luispedro.org
+.. _Luis [NAME_REDACTED]: https://luispedro.org

-.. _Fares Hedyati: http://www.eecs.berkeley.edu/~fareshed
+.. _Fares Hedyati: https://www.eecs.berkeley.edu/~fareshed

 .. _Antony Lee: https://www.ocf.berkeley.edu/~antonyl/

 .. _Martin Billinger: https://tnsre.embs.org/author/martinbillinger/

 .. _Matteo Visconti di [NAME_REDACTED]: http://www.mvdoc.me

-.. _Trevor Stephens: http://trevorstephens.com/
+.. _Trevor Stephens: https://trevorstephens.com/

 .. _Jan [NAME_REDACTED]: https://jmetzen.github.io/

@@ -156,7 +156,7 @@

 .. _Vincent Pham: https://github.com/[USER_REDACTED]

-.. _Denis Engemann: http://denis-engemann.de
+.. _Denis Engemann: https://denis-engemann.de

 .. _Anish Shah: https://github.com/[USER_REDACTED]

@@ -414,7 +414,7 @@ Bug fixes

 - Fixed handling of ties in :class:`isotonic.IsotonicRegression`.
   We now use the weighted average of targets (secondary method). By
-  `Andreas Müller`_ and `[NAME_REDACTED] <http://bommaritollc.com/>`_.
+  `Andreas Müller`_ and `[NAME_REDACTED] <https://bommaritollc.com/>`_.

 API changes summary
 -------------------

@@ -708,7 +708,7 @@ Changelog
   generates 31bits/63bits random numbers on all platforms. In addition, the
   crude "modulo" postprocessor used to get a random number in a bounded
   interval was replaced by [NAME_REDACTED] as suggested by `this blog
-  post <http://www.pcg-random.org/posts/bounded-rands.html>`_.
+  post <https://www.pcg-random.org/posts/bounded-rands.html>`_.
   Any model using the `svm.libsvm` or the `svm.liblinear` solver,
   including :class:`svm.LinearSVC`, :class:`svm.LinearSVR`,
   :class:`svm.NuSVC`, :class:`svm.NuSVR`, :class:`svm.OneClassSVM`,

Hello [USER_REDACTED], thanks for your PR, that I find overall useful.

I in total count 442 `http:` in the whole project (compared to 4700 `https:`) and I would be happy to replace them all in this PR.

But how do we deal with those pages (like the institute website from my comment) that don't use https? There must be quite a few in the docs.

I would slightly tend to try https on them as well and let user's browser's point out potential risks. Deciding this requires the opinion of another project member.

That's pretty fine, thanks for your efforts, [USER_REDACTED]!

I think this is one of the uncritical documentation improvements where one maintainer review is enough and I will merge.

One more underline fix. Otherwise LGTM.

I'm happy to have these changes.

In general/for the future though I am not sure if there is much benefit to converting everything to HTTPS. I think that not every single website you visit has to be HTTPS, in particular if you do not enter sensitive information on that website or where the path/query arguments do not contain sensitive information.

TL;DR: not against making these changes but I also think that we could do other things that have more impact.

It seems this site doesn't have a secure HTTPS configuration. At least firefox warns me about it when I try to open this page.
I am not sure what to do with cases like these. Leave them http or make the security risk explicit by [NAME_REDACTED]?

Thanks for the review! I just pushed a commit to revert the Dartmouth link to http since it doesn't support HTTPS yet.

Regarding the other 400+ links: I am happy to help with those! Would you prefer I try to update them all in this PR, or should we merge this small fix first and handle the bulk update in a separate PR?

Thanks. I know it is a lot of dull work, but ideally, you would check them all before adding the `s` and point out any problems that arise. There might be invalid links, websites that have moved and not redirected, etc. You can do it all in this PR.

Once we know the special cases (doesn't support https, has moved, doesn't exist anymore), you can sum it up and suggest what to do with these.

doc is failing because the underlines in this file are now too short.

```suggestion
-------------------------------------------
```

```suggestion
----------------------------------------
```


DOC: clarify verbose behavior in GridSearchCV and RandomizedSearchCV

This PR clarifies the behavior of the `verbose` parameter in
GridSearchCV and RandomizedSearchCV.

The documentation now:
- Explicitly documents the behavior when `verbose=0`
- Clarifies what additional information is shown as verbosity increases
- Notes that higher verbosity levels (e.g. >10) may produce additional
  progress messages depending on the execution backend

This aligns the documentation with the observed behavior discussed in
#32946.

<!--
🙌 Thanks for contributing a pull request!

👀 Please ensure you have taken a look at the contribution guidelines:
https://github.com/[USER_REDACTED]n/scikit-learn/blob/main/CONTRIBUTING.md

✅ In particular following the pull request checklist will increase the likelihood
of having maintainers review your PR:
https://scikit-learn.org/dev/developers/contributing.html#pull-request-checklist

📋 If your PR is likely to affect users, you will need to add a changelog entry
describing your PR changes, see:
https://github.com/[USER_REDACTED]n/scikit-learn/blob/main/doc/whats_new/upcoming_changes/README.md
-->

#### [NAME_REDACTED]/PRs
<!--
Example: Fixes #1234. See also #3456.
Please use keywords (e.g., Fixes) to create link to the issues or pull requests
you resolved, so that they will automatically be closed when your pull request
is merged. See https://github.com/blog/1506-closing-issues-via-pull-requests
-->

#### What does this implement/fix? Explain your changes.

#### AI usage disclosure
<!--
If AI tools were involved in creating this PR, please check all boxes that apply
below and make sure that you adhere to our [NAME_REDACTED]:
https://scikit-learn.org/dev/developers/contributing.html#automated-contributions-policy
-->
I used AI assistance for:
- [ ] Code generation (e.g., when writing an implementation or fixing a bug)
- [ ] Test/benchmark generation
- [ ] Documentation (including examples)
- [ ] Research and understanding

#### Any other comments?

<!--
Thank you for your patience. Changes to scikit-learn require careful
attention, but with limited maintainer time, not every contribution can be reviewed
quickly.
For more information and tips on improving your pull request, see:
https://scikit-learn.org/dev/faq.html#why-is-my-pull-request-not-getting-any-attention.

Thanks for contributing!
-->

@@ -1360,14 +1360,15 @@ class GridSearchCV(BaseSearchCV):
         .. versionchanged:: 0.22
             ``cv`` default value if None changed from 3-fold to 5-fold.

-    verbose : int
-        Controls the verbosity: the higher, the more messages.
+    verbose : int, default=0
+        Controls the verbosity of information printed during fitting, with higher
+        values yielding more detailed logging.

-        - >1 : the computation time for each fold and parameter candidate is
-          displayed;
-        - >2 : the score is also displayed;
-        - >3 : the fold and candidate parameter indexes are also displayed
-          together with the starting time of the computation.
+        - 0 : no messages are printed;
+        - >=1 : summary of the total number of fits;
+        - >=2 : computation time for each fold and parameter candidate;
+        - >=3 : fold indices and scores;
+        - >=10 : parameter candidate indices and START messages before each fit.

     pre_dispatch : int, or str, default='2*n_jobs'
         Controls the number of jobs that get dispatched during parallel
@@ -1744,14 +1745,15 @@ class RandomizedSearchCV(BaseSearchCV):
         .. versionchanged:: 0.22
             ``cv`` default value if None changed from 3-fold to 5-fold.

-    verbose : int
-        Controls the verbosity: the higher, the more messages.
+    verbose : int, default = 0
+        Controls the verbosity of information printed during fitting, with higher
+        values yielding more detailed logging.

-        - >1 : the computation time for each fold and parameter candidate is
-          displayed;
-        - >2 : the score is also displayed;
-        - >3 : the fold and candidate parameter indexes are also displayed
-          together with the starting time of the computation.
+        - 0 : no messages are printed;
+        - >=1 : summary of the total number of fits;
+        - >=2 : computation time for each fold and parameter candidate;
+        - >=3 : fold indices and scores;
+        - >=10 : parameter candidate indices and START messages before each fit.

     pre_dispatch : int, or str, default='2*n_jobs'
         Controls the number of jobs that get dispatched during parallel

Hi [USER_REDACTED],

thanks for the PR. The CI failures are related to this PR. Please use `pre-commit` (as described in the development guide) to fix the linting issues and then push again.

Thanks, [USER_REDACTED]. I have pushed a little wording tweak to make it all a bit shorter: I have also added a line on `verbosity=1 `which had not been mentioned and also I have seen that parameter candidate indices are only printed from `verbosity=10` on , which I found worth mentioning. Approving this PR.

Just some nits.

LGTM thanks. [USER_REDACTED] feel free to merge (when you're happy).

What about:

```suggestion
        Controls the verbosity of information printed during fitting, with higher
        values yielding more detailed logging.
```

"of the grid search" seems a bit redundant as this is the docstring for gridsearch. We can then get rid of the next sentence.

Just a question, are the logs for verbosity between 3 and 10 (e.g., 3,4,5), different? And similar question for values greater than 10, is there more information or does any number greater than 10 give the same information?

I also wonder if we should add 'includes' or 'also' (like the previous docs) to make it clear that the previous information is included as well as the the new item.

Thanks! I agree.
I have gone and modified this [USER_REDACTED].

> Just a question, are the logs for verbosity between 3 and 10 (e.g., 3,4,5), different? And similar question for values greater than 10, is there more information or does any number greater than 10 give the same information?

From 3 to 9, as well as above 10, it is all the same information.

> I also wonder if we should add 'includes' or 'also' (like the previous docs) to make it clear that the previous information is included as well as the the new item.

We had this at first, but I then decided to go with  `>=number` meaning "every verbosity larger than `number` displays this information".


FIX array API support when `pos_label=None` for brier score metrics

#### [NAME_REDACTED]/PRs

#### What does this implement/fix? Explain your changes.
As discussed in https://github.com/[USER_REDACTED]n/scikit-learn/pull/32422/changes#r2575666004 , add array API handling to the case when `pos_label=None` and `y_true` is not string.

Also adds test.

#### AI usage disclosure

I used AI assistance for:
- [ ] Code generation (e.g., when writing an implementation or fixing a bug)
- [ ] Test/benchmark generation
- [ ] Documentation (including examples)
- [x] Research and understanding

#### Any other comments?
This highlights again the inconsistency of `pos_label` handling, see longstanding issue #10010 - details here: https://github.com/[USER_REDACTED]n/scikit-learn/issues/10010#issuecomment-1826133710 - would be great to get any movement/thoughts here!

cc [USER_REDACTED], [USER_REDACTED]

@@ -0,0 +1,3 @@
+- Fixes how `pos_label` is inferred when `pos_label` is set to `None`, in
+  :func:`sklearn.metrics.brier_score_loss` and
+  :func:`sklearn.metrics.d2_brier_score`. By :user:`[NAME_REDACTED] <lucyleeow>`.

@@ -3614,10 +3614,11 @@ def _validate_binary_probabilistic_prediction(y_true, y_prob, sample_weight, pos
     try:
         pos_label = _check_pos_label_consistency(pos_label, y_true)
     except ValueError:
-        classes = np.unique(y_true)
-        if classes.dtype.kind not in ("O", "U", "S"):
-            # for backward compatibility, if classes are not string then
-            # `pos_label` will correspond to the greater label
+        xp_y_true, _ = get_namespace(y_true)
+        classes = xp_y_true.unique_values(y_true)
+        # For backward compatibility, if classes are not string then
+        # `pos_label` will correspond to the greater label.
+        if not (_is_numpy_namespace(xp_y_true) and classes.dtype.kind in "OUS"):
             pos_label = classes[-1]
         else:
             raise

@@ -45,12 +45,13 @@
 from sklearn.preprocessing import LabelBinarizer, label_binarize
 from sklearn.tree import DecisionTreeClassifier
 from sklearn.utils._array_api import (
-    device as array_api_device,
-)
-from sklearn.utils._array_api import (
+    _get_namespace_device_dtype_ids,
     get_namespace,
     yield_namespace_device_dtype_combinations,
 )
+from sklearn.utils._array_api import (
+    device as array_api_device,
+)
 from sklearn.utils._mocking import MockDataFrame
 from sklearn.utils._testing import (
     _array_api_for_tests,
@@ -3743,3 +3744,30 @@ def test_probabilistic_metrics_multilabel_array_api(
         metric_score_xp = prob_metric(y_true_xp, y_prob_xp, sample_weight=sample_weight)

     assert metric_score_xp == pytest.approx(metric_score_np)
+
+
+[USER_REDACTED].mark.parametrize(
+    "array_namespace, device_, dtype_name",
+    yield_namespace_device_dtype_combinations(),
+    ids=_get_namespace_device_dtype_ids,
+)
+[USER_REDACTED].mark.parametrize("prob_metric", [brier_score_loss, d2_brier_score])
+def test_pos_label_in_brier_score_metrics_array_api(
+    prob_metric, array_namespace, device_, dtype_name
+):
+    """Check `pos_label` handled correctly when labels not in {-1, 1} or {0, 1}."""
+    # For 'brier_score' metrics, when `pos_label=None` and labels are not strings,
+    # `pos_label` defaults to the largest label.
+    xp = _array_api_for_tests(array_namespace, device_)
+    y_true_pos_1 = xp.asarray(np.array([1, 0, 1, 0]), device=device_)
+    # Result should be the same when we use 2's for the label instead of 1's
+    y_true_pos_2 = xp.asarray(np.array([2, 0, 2, 0]), device=device_)
+    y_prob = xp.asarray(
+        np.array([0.5, 0.2, 0.7, 0.6], dtype=dtype_name), device=device_
+    )
+
+    with config_context(array_api_dispatch=True):
+        metric_pos_1 = prob_metric(y_true_pos_1, y_prob)
+        metric_pos_2 = prob_metric(y_true_pos_2, y_prob)
+
+    assert metric_pos_1 == pytest.approx(metric_pos_2)

LGTM. Thanks [USER_REDACTED]

That looks good to me, thank you [USER_REDACTED].
I only have two nits on the documentation side.

Note this relies on lazy evaluation - if `_is_numpy_namespace(xp_y_true)` is False, the second condition is not evaluated.

Also note when namespace is not numpy, this evaluates to True.

Maybe this is not needed, see #32924 . I'm happy to remove.

I think the modules we are supporting do return sorted values. I am not sure about `array-api-strict` but if we don't get errors, it probably returns sorted results too.

https://docs.pytorch.org/docs/stable/generated/torch.unique.html#torch-unique

https://docs.cupy.dev/en/stable/reference/generated/cupy.unique.html

I find "fix inference" pretty unclear. Maybe like this:

```suggestion
- Fixes how `pos_label=None` is inferred in
  :func:`sklearn.metrics.brier_score_loss` and
  :func:`sklearn.metrics.d2_brier_score`.
  By :user:`[NAME_REDACTED] <lucyleeow>`.
```

I would suggest to move the comment in front of the condition, as we usually comment in the line before.

Indeed, I will remove it as we don't sort elsewhere either and it is not a problem for the array libraries we support.

(array api strict just uses numpy so, it sorts too)